Book 6 — HR & Payroll (Index)¶
Bizwiz Guide · Book 6 of 7 · Review instrument Where people become ledger entries: the employee master, the monthly payroll run that every HR sub-module feeds, the statutory/benefit deductions and staff Sacco that ride on it, and the leave/attendance signal that scales the whole thing up or down.
This book covers the people-and-pay end of Bizwiz. If Book 4 is where money lands as accounting entries, Book 6 is where the single largest recurring money-out event — the monthly payroll run — is assembled, approved, and posted. Read it as: one calculation engine (PayrollCalculationService) builds each payslip; everything else in the book is either an input to that calculation or a consumer of its output. Configurations, the employee master, leave, attendance, statutory schemes, penalties, and the Sacco all converge on the payroll month; the payroll month then posts, once, into the Book 4 wa_gl_trans spine.
| # | Chapter | One-line scope |
|---|---|---|
| 23 | Employees & Payroll | HR configs, employee/casual master, the monthly payroll run, casuals-pay (M-Pesa B2C), commission/incentives/payouts, reports, and the payroll → GL posting |
| 24 | Benefits & Deductions | Statutory (NSSF/PAYE/SHIF/Housing), salary advances, penalties, HELB, medical cover, benevolent fund — how each attaches to a payslip and remits |
| 25 | Leave & Attendance | Leave maker-checker, holidays/off-days, ZKTeco biometric + remote clocking, and the attendance-ratio proration that scales gross pay |
| 26 | Sacco | Staff savings-and-credit co-op: shares, loan products, drafts→approvals→disbursement, and payroll-deduction recovery |
The payroll spine — everything feeds one calculation, one posting¶
flowchart TD
subgraph INPUTS[Inputs assembled per employee, per month]
EMP[Employee / Casual master · Ch23<br/>employees, casuals]
ATT[Attendance & Leave · Ch25<br/>processed_attendance → attendance_status]
STAT[Statutory rate tables · Ch24<br/>nssfs · Paye · Shif · HousingLevy]
DED[Obligations · Ch24 + Ch26<br/>salary advances · EmployeePenalty · HELB<br/>medical · benevolent · Sacco shares & loans]
end
CALC[PayrollCalculationService<br/>build each payslip]
ATT -->|attendance_ratio prorates<br/>ALL gross buckets| CALC
EMP --> CALC
STAT -->|written as COLUMNS| CALC
DED -->|written as PIVOT ROWS<br/>capped at remaining balance| CALC
CALC --> PMD[(payroll_month_details<br/>+ payroll_month_detail_deductions)]
PMD -->|HR approval of the month| POST
subgraph POST[Post-approval fan-out]
REC[PostPayrollService<br/>writes recovery ledgers:<br/>SalaryAdvancePayment · EmployeeDeductionTransaction<br/>sacco_loan_transactions · BenevolentFund]
GLP[PostPayrollGLService · PM-##### · per branch<br/>DR gross · CR net · CR each statutory/scheme liability]
SP[StatutoryPayment rows<br/>PAYE/NSSF/SHIF/Housing/HELB/Sacco/Benevolent<br/>queued for remittance]
end
POST --> GL[(wa_gl_trans · Book 4<br/>signed-amount trial-balance spine)]
CASH[Casuals Pay · Ch23<br/>MpesaDisbursementService · Daraja B2C] -->|complete callback<br/>DR 56002-033 / CR bank 988329| GL
The one-sentence version: PayrollCalculationService builds each payslip by prorating gross pay by an attendance ratio and then subtracting statutory amounts (as columns) and every other obligation (as capped pivot rows keyed by Deduction.code); on HR approval the month fans out into recovery sub-ledgers, a per-branch wa_gl_trans journal, and StatutoryPayment remittance queue — so leave, attendance, deductions, penalties, and Sacco are all just inputs to one calculation and one posting.
Cross-chapter & cross-book seams¶
Within Book 6 (everything routes through the payroll calc):
- Ch25 → Ch23 (attendance scales pay): AttendanceService collapses punches + leave/off-day overrides into processed_attendance.attendance_status; PayrollCalculationService::calculateAttendance counts days whose status is in the tenant setting allowed_attendance_statuses, forms attendance_ratio = attended / working, and prorates every gross bucket. Absence has no deduction line — it is pure proration. Paid-vs-unpaid leave is a config decision (include/exclude 'Leave' from allowed_attendance_statuses), not a LeaveType column.
- Ch24 → Ch23 (two deduction mechanisms): statutory (NSSF/PAYE/SHIF/Housing) are written as columns on payroll_month_details from rate tables; everything else is a pivot row in payroll_month_detail_deductions, gated on an active Deduction catalogue code and capped at the obligation's remaining balance, tagged with salary_advance_id / employee_penalty_id / sacco_loan_id.
- Ch26 → Ch24 → Ch23 (Sacco rides the deduction engine): Sacco has no own collection — it injects DED-SACCO-SHARES, DED-SACCO-LOAN, DED-SACCO-LOAN-INTEREST lines (principal/interest split by SaccoLoanService), tagged sacco_loan_id. Sacco keeps an operational sub-ledger (sacco_loan_transactions / sacco_share_transactions) but no separate double-entry ledger.
To other books:
- → Book 4 (Finance — the CRITICAL outbound seam): the payroll month posts once, per branch, into wa_gl_trans via PostPayrollGLService (PAYROLL series, doc PM-#####), with a read-only rebuild twin PayrollMonthGlPostingService. This is the HR→GL destination seam that mirrors Book 3 AP and Book 1 Sales as a major feeder. Sacco and every statutory/benefit scheme reach the GL only indirectly through this service — none of the HR sub-controllers write wa_gl_trans themselves.
- → Book 4 (M-Pesa disbursement): Casuals Pay disburses over Daraja B2C (MpesaDisbursementService), booking DR 56002-033 / CR bank 988329 on the complete callback — the same live Daraja dependency as Book 4 petty cash.
- ← Book 5 (Fleet — OPEN LOOP NOW RESOLVED): the driver-comp loop left open in Book 5 is consumed by payroll — but not by name. Fuel/mileage/short-banking/late-shift penalties are minted as polymorphic EmployeePenalty rows (penalty_type/penalty_id) at is_approved=0 by their source modules (FuelEntryApprovalController:445, InboundDeliveryController:1855, PosBankingController:7899). A second HR approval on the Employee-Penalties page (setting monthly_pay_amount) is the missing half that lets PayrollCalculationService pick them up as a DED-EMPLOYEE-PENALTY pivot row. This is why Ch23's grep for driver_tyre_incentive came back empty — the seam is the polymorphic penalty, not a direct fleet-table reference. (The tyre incentives — the positive/earnings side — were still not found flowing into the payroll earnings; that half of the loop remains open — see below.)
Two things that look connected but aren't (yet)¶
- Driver incentives vs driver penalties. Penalties reach payroll via
EmployeePenalty(resolved above). The incentive/earnings side (driver_tyre_incentives, fuel/service incentives) was not found flowing intopayroll_month_detail_earnings— payroll/commission/incentive/payout code does not reference them. The negative half of the Book 5 loop is still open. - Three unrelated "device" tables. Ch25's HR clocking devices are
hr_devices(ZKTeco biometric terminals,App\Models\HrDevice) — distinct from Book 5's GPStracking_devices/TrackingDeviceand the repairdevices/Device. Three "device" concepts coexist; do not conflate.
Consolidated open questions (the review agenda)¶
Grouped from each chapter's §7. CODE-PROVEN items are asserted as fact in the chapters; the below need human/dev confirmation.
Configuration / "which is live per client":
- GL account mapping is DB-config, not code — PayrollGlMapping → WaChartsOfAccount resolves which COA is gross-pay expense vs net-pay/liability; the literal codes are not statically verifiable. Same for the exact wa_gl_trans row columns (that's the Book 4 GL layer).
- Which disbursement binding is active per tenant — AppServiceProvider binds MpesaDisbursementService, but a conditional binding (~line 182) references DarajaDisbursementService/PesaFlowDisbursementService; the tenant-selection logic was not fully traced.
- allowed_attendance_statuses per tenant decides paid-vs-unpaid leave and the working-day denominator — the standing "which is live" question, now applied to attendance.
- Interest & eligibility models per Sacco product — three interest models (reducing_balance, compound_fixed, simple_fixed) and three eligibility models (shares_based, salary_based, supplier_determined) are product-configurable; which each tenant uses is config.
Data-integrity quirks / possible bugs to verify:
- Casuals GL hardcodes branch 10 (MAKONGENI), bank 988329, cashier_id = 1 — intentional single-till design or a latent multi-branch bug? Needs product confirmation.
- NSSF & Housing-Levy triple posting — handleNSSF/handleHousingLevy each post two employee credits to the same account + one employer debit; possible double-count worth a Book 4 GL audit.
- EmployeePenalty.employee_id dual-key ambiguity — code treats it as users.id while relations/migrations mix employee_id and user_id; likely source of subtle bugs, worth a dedicated audit. (Same namespace-split risk appears in leaves FKs mixing App\Model\User and App\Models\*.)
- Sacco management ↔ hq_gm naming drift — permission sacco-loans___management_approve maps to hq_gm_approved_* columns; intentional or latent bug?
- Legacy dead tables — wa_sacco / wa_payroll_sacco (Sacco) appear to have no live references; confirm and schedule for the legacy-inventory pass.
Ownership / boundaries & not-fully-traced:
- Route-level permission enforcement — the *___view/*_approve strings appear in nav Blade; middleware('permission:…') was not confirmed on the hr.php route groups. Verify controllers hard-enforce (via checkPermission()) rather than relying on nav hiding. (Recurs across the whole guide.)
- Incentive earnings → payslip — whether commission/incentive earnings flow into payroll_month_detail_earnings for permanent staff or only via outsourced "Send to Accounts" payouts was not conclusively traced.
- Holiday → payroll effect — holidays are stored/branch-scoped, but whether they reduce the working-day denominator lives inside payrollWorkingWeekdays()/payrollWorkingDaysInPeriod() helpers not opened here.
- Remote clocking has no geofence — remoteClocking records only a timestamp; no lat/long/geofence validation found in read paths (strongly-indicated inference; mobile/API layer not exhaustively searched). It fabricates real attendances rows from self-reported punches on approval.
- Casual pay-per-day formula and the exact loan status='completed' trigger, physical cash delivery for Sacco disbursement, and the recalled/LeaveRecalls/LeaveReversal lifecycle were each flagged as inferred, not pinned.
- Benevolent-fund ownership boundary — its StatutoryPayment is created in PostPayrollGLService (~line 359) though benevolent is a Ch24 concern; confirm the boundary. No balance cap found in the calc.
Recurring Bizwiz patterns reinforced by Book 6¶
- One engine, many feeders (again).
PayrollCalculationService+PostPayrollGLServiceare to HR whatGlTransactionServiceis to Finance and thepayment_voucher_itemsengine is to AP: a single durable calculation/posting core that every sub-module funnels into. Statutory columns vs. capped pivot rows is the one clean abstraction that absorbs advances, penalties, HELB, medical, benevolent, and Sacco alike. - Approval queues everywhere — the strongest "approvers not curators" surface yet. Leave maker-checker (branch→HQ), remote-clocking approval (fabricates attendance from self-report, no fact verification), remote-login grants, manual-attendance approval, penalty second-approval, Sacco loan drafts→approvals→top-up-approvals→NPL/recovery. Humans mostly sign booleans on records the system already assembled — and several sign-offs verify nothing (prime automation + anomaly-detection targets).
- Config- and flag-driven behaviour. Paid-vs-unpaid leave, allowed attendance statuses, statutory rate tables, Sacco interest/eligibility models, and the disbursement-service binding all vary the same business event per tenant. Document both branches; "which is live per client" remains the standing method.
- Real external integrations + hardcoded seams. Daraja B2C (casuals) and ZKTeco biometric cloud (
41.90.112.98:82/.99:82) are live dependencies — alongside hardcoded branch/bank/cashier constants in the casuals GL path that read as single-till assumptions leaking into multi-branch code. - Mid-modernization layering & legacy coexistence. Modern service-class payroll (
PayrollCalculationService,SaccoLoanService, GL posting services) sits over legacy dead tables (wa_sacco,wa_payroll_sacco) and namespace splits (App\ModelvsApp\Models) — the same "modern surface, legacy underneath" seen in Books 2–5.
Book 6 complete: 4 chapters (23/24/25/26) + this index, assembled from static analysis of the bizwiz monolith. All four verified on disk (2,316 / 2,934 / 3,083 / 2,845 words; all 8 template sections; 2–3 Mermaid diagrams each). Headline: the monthly payroll run is one calculation engine that prorates gross by attendance and subtracts capped, catalogue-coded obligations, then posts once per branch into the Book 4 wa_gl_trans spine — and it resolves the Book 5 driver-penalty open loop (via polymorphic EmployeePenalty + a second HR approval), while leaving the driver-incentive earnings half still open. Next in the fan-out: Book 7 — Platform & Admin (Ch27 System Admin, 28 CRM, 29 Help Desk, 30 Communication Centre), then the Manufacturing/Laboratory placeholders + a cross-module handoff map.