Chapter 24: Benefits & Deductions¶
Book 6 (HR & Payroll) · Bizwiz ERP · Static-analysis-derived documentation Scope: everything added to or subtracted from pay besides base salary — salary advances, penalties, statutory deductions (NSSF, PAYE, SHIF, Housing Levy), and managed benefit/loan schemes (HELB, Medical Cover, Benevolent Fund). The core payroll run, employee master, commission and casuals pay live in Ch23; Sacco in Ch26.
1. Purpose¶
Plain language. When Bizwiz runs monthly payroll, an employee's gross pay is whittled down to net pay by a stack of deductions. This chapter covers everything in that stack that is not base salary:
- Salary advances — the company lends an employee money now, then claws it back over several months by deducting instalments from payslips.
- Penalties — money charged against an employee for a fault (fuel over-consumption by a driver, a mileage variance on a delivery, a short banking, a late shift). These are approved, given a monthly recovery cap, and clawed back like a mini-loan.
- Statutory deductions — the Kenyan taxman's cut: PAYE (income tax), NSSF (pension), SHIF (health, formerly NHIF), and the Housing Levy. Bizwiz calculates these from configurable rate tables and posts them to the general ledger as liabilities the company must remit.
- Managed schemes — opt-in or scheme-based recurring deductions with their own balances and remittance: HELB (student-loan repayment to the government agency), Medical Cover (private health-plan premiums), and the Benevolent Fund (a welfare contribution).
Each of these has the same skeleton: a per-employee enrollment/status record that says "how much per month," a linkage into the monthly payroll run that produces the actual per-payslip amount, a post-payroll step that records the recovery/contribution against a running balance, and a remittance view + GL/AP posting that lets Finance actually pay the third party.
Technical depth. The seam that unifies everything is the pivot table payroll_month_detail_deductions (model PayrollMonthDetailDeduction, aliased on PayrollMonthDetail as ->deductions()). Every deduction — statutory, scheme, advance, penalty, or user-defined — either becomes a column on payroll_month_details (the four statutory ones: paye, nssf, shif, housing_levy) or a pivot row keyed by a Deduction record's code (everything else). The engine is App\Services\PayrollCalculationService; the post-processing/recovery engine is App\Services\PostPayrollService; the GL/AP posting engine is App\Services\PostPayrollGLService.
The Deduction catalogue (deductions table) drives it all through well-known codes: DED-SALARY-ADVANCE, DED-EMPLOYEE-PENALTY, DED-HELB, DED-MEDICAL-COVER, DED-BENEVOLENT, DED-NSSF-VOLUNTARY-CONTRIBUTION, DED-LOAN-RECOVERIES, DED-SACCO-LOAN (Ch26). Each is is_active / system_reserved flagged.
2. Users & Roles¶
Plain language. The nav gates each area behind a permission string, with role_id == 1 (super-admin) always allowed. The three families are:
- Advance and Penalties — needs
advance-penalties___viewfor the group, thensalary-advance___view,employee-penalties___view, andadvance-penalties-transactions___viewfor the sub-pages. - Managed Deductions — the parent shows if the user has any of
payroll-nssf___view,payroll-helb___view,payroll-medical-cover___view,payroll-benevolent___view; each scheme's Employee-status and Remittance pages then check their own…-employee-status___view/…-remittance___viewvariants.
Approvals are a distinct capability: salary advances and penalties each have an approve/reject step, and driver/operational penalties raised elsewhere land here unapproved and must be approved on the Employee-Penalties page before they ever hit a payslip.
Technical depth. Permission strings observed in resources/views/admin/includes/sidebar_includes/hr.blade.php (checked as isset($my_permissions['…']), OR-ed with $logged_user_info->role_id == 1):
| Area | Group perm | Sub-page perms |
|---|---|---|
| Advance & Penalties | advance-penalties___view |
salary-advance___view, employee-penalties___view, advance-penalties-transactions___view |
| NSSF | payroll-nssf___view |
payroll-nssf-employee-status___view, payroll-nssf-remittance___view |
| HELB | payroll-helb___view |
payroll-helb-employee-status___view, payroll-helb-remittance___view |
| Medical Cover | payroll-medical-cover___view |
payroll-medical-cover-plans___view, payroll-medical-cover-enrollments___view, …-remittance___view |
| Benevolent | payroll-benevolent___view |
payroll-benevolent-employee-status___view, …-remittance___view |
Routes live in routes/modules/hr.php under the admin/hr/payroll/… prefix (advance/penalty: lines 229–270; NSSF: 443–455; HELB: 458–469; Medical Cover: 473–487; Benevolent: 489–498). Controllers: HrSalaryAdvanceController (advances and penalties and deduction balances — all three menus route to this one ~138 KB controller), NssfManagementController, HelbManagementController, MedicalCoverController, BenevolentFundManagementController. Approval writers set is_approved, approved_by, approved_at (advances) or flip EmployeePenalty.is_approved (penalties). No explicit middleware('permission:…') guard was found on these route groups in hr.php — gating appears to be view-level (nav) plus in-controller checks, so the permission strings are best read as nav visibility rather than hard route guards (see §7).
3. Processes¶
Plain language. There are two recurring lifecycles worth drawing: (a) how a deduction attaches to a payslip and is later recovered, and (b) how a penalty is born elsewhere in the ERP, funnelled here, approved, and recovered. Below both.
3a. Deduction → payslip → recovery → remittance¶
flowchart TD
subgraph Setup["Per-employee setup (before payroll)"]
A1[Salary advance approved + disbursed]
A2[EmployeeHelb / MedicalCoverEnrollment / EmployeeBenevolentFund is_active]
A3[EmployeePenalty is_approved + monthly_pay_amount set]
end
subgraph Calc["PayrollCalculationService (per payslip)"]
B1[calculateSystemDeductions] --> B2[Look up Deduction by code, is_active]
B2 --> B3[Compute per-employee amount<br/>capped at remaining balance]
B3 --> B4["deductions()->create pivot row<br/>with salary_advance_id / employee_penalty_id"]
B0[Statutory: NSSF/PAYE/SHIF/HousingLevy] --> B5[Written as COLUMNS on payroll_month_details]
end
subgraph Post["PostPayrollService (after approval)"]
C1[processSalaryAdvances -> SalaryAdvancePayment 'recovery']
C2[processPenalties -> EmployeeDeductionTransaction -amount]
C3[processHelb -> recordCredit]
C4[processMedicalCover -> recordCredit]
C5[processBenevolent -> BenevolentFund row]
end
subgraph GL["PostPayrollGLService (GL + AP)"]
D1[handleNSSF/PAYE/SHIF/HousingLevy/Helb<br/>credit liability GL account per branch]
D2[createXStatutoryPayment -> StatutoryPayment + items]
end
Setup --> Calc --> Post
Calc --> GL
D2 --> E[Remittance pages read StatutoryPayment / scheme transactions]
3b. Penalty lifecycle (incl. driver-comp open loop from Book 5)¶
flowchart LR
subgraph Origin["Origin events (other modules)"]
F1[DriverFuelPenalty approved<br/>FuelEntryApprovalController]
F2[InboundDeliveryMileagePenalty approved<br/>InboundDeliveryController]
F3[Short banking<br/>PosBankingController]
F4[Late salesman shift<br/>EmployeePenaltyRequest approved]
F5[Manual/bulk upload<br/>HrSalaryAdvanceController]
end
F1 & F2 & F3 & F4 & F5 --> G[EmployeePenalty row created<br/>penalty_type/penalty_id polymorphic<br/>is_approved = 0]
G --> H[HR reviews on Pending Penalties page]
H -->|approve + set monthly_pay_amount| I[EmployeePenalty.is_approved = 1]
H -->|reject| J[rejection_reason set]
H -->|waive| K[EmployeeDeductionTransaction type=waiver]
I --> L[Picked up by PayrollCalculationService<br/>as DED-EMPLOYEE-PENALTY pivot row]
L --> M[Recovered: EmployeeDeductionTransaction type=recovery -amount]
Technical depth — attachment (the load-bearing seam). PayrollCalculationService::calculateSystemDeductions() (app/Services/PayrollCalculationService.php:~200–504) runs per employee inside the payroll build. For each scheme it: (1) looks up the Deduction row by code where is_active=true; (2) if present, finds the per-employee obligation; (3) computes the amount, capped at the remaining balance; (4) writes a pivot row via $payrollMonthDetail->deductions()->create([...]); (5) adds to $totalSystemReserved. Specifics:
- Salary advance (lines 312–352): iterates every active advance (
is_approved, disbursed-or-statutory-paid,status != 'paid', ordered oldest-first). Monthly instalment =amount / repayment_months, capped atremaining_balance. One pivot row per advance, taggedsalary_advance_id. Skips advances past theirrepayment_monthswindow (monthsSinceApproval >= repayment_months). - Employee penalty (lines 354–389): only penalties with
is_approved=trueand a non-null positivemonthly_pay_amount. Amount =min(monthly_pay_amount, outstanding_balance). One pivot row per penalty, taggedemployee_penalty_id. Note the employee key here is a User id (User::where('employee_id', …)->pluck('id')), not the Employee id — penalties are stored againstusers.id. - HELB (432–453): single
EmployeeHelbwhereis_activeandstatus='active'; amount fromgetPayrollDeductionAmount()=min(monthly_deduction, current_balance). - Medical cover (456–478): single active
MedicalCoverEnrollment;getPayrollDeductionAmount(). - Benevolent (480–501): single active
EmployeeBenevolentFund; flatmonthly_deduction(no balance cap — it's an open-ended contribution). - Statutory (NSSF/PAYE/SHIF/Housing Levy, lines ~60–130) are not pivot rows — they're computed from rate tables and stored as columns
nssf,shif,housing_levy,payeonpayroll_month_details. PAYE is derived last:taxablePay = payeGross − nssf − housingLevy − shif − nssfVoluntary, thencalculatePaye(...)withtaxReliefAmount. SHIF has a KES 300 floor (max(round(gross*rate/100,2), 300)). NSSF voluntary contribution is a pivot row (DED-NSSF-VOLUNTARY-CONTRIBUTION).
Technical depth — recovery (PostPayrollService, runs after the month is approved).
- processSalaryAdvances() (287–330): for each pivot row with a salary_advance_id, creates a SalaryAdvancePayment (type='recovery') and updates the advance status to paid (if is_fully_paid) or partially_paid.
- processPenalties() (339–383): for each pivot row with employee_penalty_id, writes an EmployeeDeductionTransaction with negative amount and type='recovery'. Balance is derived, not stored: EmployeePenalty::outstanding_balance = SUM(amount) over its transactions (charges positive, recoveries/waivers negative).
- processHelb() (519–567) / processMedicalCover() (572–…): call recordCredit(...) on the obligation, then checkAndMarkCompleted() when the balance hits zero.
- processBenevolent() (478–514): creates a BenevolentFund transaction row stamped with payroll_month_id / payroll_month_detail_id.
- Every processor wraps its body in try/catch and, on failure, writes a PostPayrollProcessingFailure row (component = salary_advance / penalty / helb / benevolent / …) so a partial failure is auditable rather than silent.
4. Tables Touched & Key Data¶
Plain language. The data model is a set of "obligation + ledger" pairs. Each scheme has a header row (the enrollment/status) and a transactions table that acts as its ledger; balances are usually computed from the ledger, not stored, which keeps them honest but means a corrupt ledger silently misreports a balance.
Technical depth.
| Table | Model | Role / notable columns |
|---|---|---|
payroll_month_detail_deductions |
PayrollMonthDetailDeduction |
The universal pivot. Base migration (2024_10_11) had only deduction_id, amount. Later migrations bolted on sacco_loan_id (2025_12_20), salary_advance_id + employee_penalty_id (2026_05_19), recovery_obligations_data JSON (2026_02_25), balance snapshot (2026_09_03). Quirk: the same physical row is reused to point at whichever obligation applies. |
salary_advances |
SalaryAdvance |
amount, repayment_months, status, is_approved/is_disbursed, statutory_payment_id. remaining_balance = total_disbursed − total_paid, falling back to amount when no disbursement ledger row exists (legacy advances). |
salary_advance_payments |
SalaryAdvancePayment |
Ledger: type ∈ {disbursement, recovery, direct-payment}. Recoveries created by PostPayrollService. |
employee_penalties |
EmployeePenalty |
Polymorphic morphs('penalty') → penalty_type/penalty_id. amount is integer (rounded). monthly_pay_amount = recovery cap. is_approved added later (2025_06_18); penalty_id made nullable same day (manual penalties have no source). Quirk: employee_id holds a users.id in practice, not an employees.id. |
employee_deduction_transactions |
EmployeeDeductionTransaction |
Penalty ledger. type ∈ {recovery, waiver, …}; amount signed (charges +, recoveries −). Composite index (employee_penalty_id, amount) for fast SUM. |
employee_deduction_balances |
EmployeeDeductionBalance |
The "Penalty Ledger" nav page. running_balance, monthly_pay_amount, last_activity_at. Used for the aggregate per-employee penalty view / monthly-amount bulk edits. |
nssfs |
Nssf |
Rate tiers (from/to bands, guarded=[]). Nssf::orderBy('from')->first() = Tier 1 max. |
employee_helb |
EmployeeHelb |
monthly_deduction, is_active, status. current_balance = SUM(debits) − SUM(credits) from employee_helb_transactions. |
benevolent_fund / employee_benevolent_fund |
BenevolentFund / EmployeeBenevolentFund |
Header (monthly_deduction, is_active) + contribution ledger (positive amounts). current_balance = SUM(amount). |
medical_cover_plans / medical_cover_enrollments / medical_cover_transactions |
MedicalCoverPlan / MedicalCoverEnrollment / MedicalCoverTransaction |
Plan has annual_premium, company_contribution_percentage. Enrollment drives getPayrollDeductionAmount(). |
statutory_payments / statutory_payment_items |
StatutoryPayment / StatutoryPaymentItem |
The AP/remittance vehicle created after GL posting for PAYE/NSSF/SHIF/Housing/HELB/Benevolent/NetPay/NITA/Sacco. Has supplier, bank_id, declaration_number, status. Salary advances also link via salary_advances.statutory_payment_id (disbursement demand). |
payroll_gl_mappings |
PayrollGlMapping |
component_code → gl_account_id (employee side) + employer_gl_account_id (company side) + supplier. Drives the GL/AP posting. |
5. Interactions With Other Modules¶
Plain language. This chapter is the confluence point for several other modules: it consumes penalties raised in Fleet/Fuel/POS/Sales (Book 5), feeds the payroll run (Ch23), and pushes liabilities into the General Ledger (Book 4) and Accounts Payable.
Deductions → payroll run (Ch23). Covered in §3. The single seam is payroll_month_detail_deductions (pivot rows) + four columns on payroll_month_details. PayrollMonthController reads these back to render per-column payslip tooltips, matching pivot rows by deduction_id and, for advances/penalties with >1 row, exploding a per-item breakdown popover keyed on salary_advance_id / employee_penalty_id (PayrollMonthController.php:~197–391, 1993–2007). netPay = adjustedCashGross − (paye + nssf + shif + housingLevy + totalAllDeductions).
Remittance → GL / AP (Book 4). PostPayrollGLService::handleX() posts, per branch, a credit to a liability GL account for each statutory/scheme deduction (account resolved via PayrollGlMapping.component_code → WaChartsOfAccount; NSSF & Housing Levy additionally post an employer-match debit to the company-contribution account). Immediately after, createXStatutoryPayment() (PostPayrollGLService.php:307–360) materialises a StatutoryPayment + StatutoryPaymentItem set — this is the record the Remittance Reports pages read, and the object Finance turns into an actual outbound payment (it carries supplier, bank_id, declaration_number). So the liability is booked to the GL at payroll-post time and cleared when the StatutoryPayment is paid. HELB, Medical Cover and Benevolent recoveries additionally maintain their own scheme ledgers so their remittance pages can reconcile per-employee. (The exact wa_gl_trans write happens inside the injected $this->gl->postPayrollCredit/Debit helper — see Book 4 for that layer.)
Driver-comp open loop from Book 5 — RESOLVED. The Book 5 question was whether fuel/tyre/mileage driver penalties (DriverFuelPenalty, InboundDeliveryMileagePenalty, etc.) that merely flip a status ever reach payroll. They do, via the polymorphic EmployeePenalty funnel. When such a source penalty is approved, the approving controller also creates an EmployeePenalty row pointing back at it:
- FuelEntryApprovalController::approvePenalty() (app/Http/Controllers/FuelEntryApprovalController.php:445–457): EmployeePenalty::create(['penalty_type' => 'fuel_penalty', 'penalty_id' => $penalty->id, 'employee_id' => $penalty->driver_id, 'amount' => round((actual−std)*penalty_per_liter), …]).
- InboundDeliveryController (app/Http/Controllers/Admin/InboundDeliveryController.php:1855–1868): penalty_type => InboundDeliveryMileagePenalty::class, amount => round(penalty_amount).
- Also: PosBankingController (short-banking → penalty_type => 'App\Model\ShortBankingComment', ~7899), ShiftController (late_shift), EmployeePenaltyRequestController (salesman shift, via $shift->employeePenalties()->save(...)), plus manual/bulk upload and "Balance brought forward" migration rows in HrSalaryAdvanceController.
Crucially, the EmployeePenalty is created with is_approved = 0 (see the InboundDelivery comment at line 1868: "the EmployeePenalty row just created is still unapproved… approval happens on the Employee Penalties page"). So the source-module approval is only step 1; an HR user must approve it here and set monthly_pay_amount before PayrollCalculationService will pick it up. That's the missing half of the Book 5 loop: the penalty flips its own status in the source module, mints an unapproved EmployeePenalty, and only becomes a real payroll deduction after a second HR approval that also sets the monthly recovery cap.
Salary-advance recovery loop. Advance is requested → approved (is_approved) → disbursed (a SalaryAdvancePayment type='disbursement', possibly via a StatutoryPayment disbursement demand referenced by salary_advances.statutory_payment_id). Each subsequent payroll: PayrollCalculationService adds a pivot row for min(amount/repayment_months, remaining_balance); PostPayrollService::processSalaryAdvances() writes a SalaryAdvancePayment type='recovery' and flips status to partially_paid/paid. remaining_balance = total_disbursed − total_paid. The Employee Ledger page (salary-advances.employee-ledger) reads these payment rows.
Sacco (Ch26). Same pivot mechanism (DED-SACCO-LOAN, DED-SACCO-LOAN-INTEREST, DED-LOAN-RECOVERIES) — deliberately out of scope here but structurally identical; the advance/penalty handling explicitly "mirrors SACCO loan handling."
6. Alternatives & Variants¶
Plain language. Almost everything is toggled by a Deduction catalogue row and per-employee/branch overrides, so a tenant can turn any scheme on or off, exempt individuals, and set rates centrally.
Technical depth.
- Master on/off per scheme: each scheme only runs if its
Deductionrow exists withis_active=true(DED-HELB,DED-MEDICAL-COVER,DED-BENEVOLENT,DED-SALARY-ADVANCE,DED-EMPLOYEE-PENALTY). Delete/deactivate the catalogue row and the whole scheme silently stops attaching (no error) — a powerful but foot-gun-prone kill switch. - 3-tier hierarchy for user-defined deductions:
calculateDeductionForEmployee()resolves amount from employee setting → branch setting → global default (DeductionEmployeeSetting,DeductionBranchSetting,Deduction). An employee setting withis_active=false= per-employee exemption.amount_type∈ {fixed_amount,percentage} (percentage basis =basic_pay). - Mandatory vs opt-in: NSSF/PAYE/SHIF/Housing are mandatory statutory columns; NSSF has opt-out fields (
nssf_tier2_opted_out,nssf_pension_provider_idon employees; bulk/import opt-out routes inNssfManagementController) — a Tier-2 opt-out snapshots the private pension provider onto the payslip. HELB/Medical/Benevolent are opt-in via theiris_activeenrollment rows. - Statutory-rate config: rates come from dedicated tables (
nssfstiers,Payetiers,Shifrate,HousingLevyrate) validated up front — if any is missing, the run errors ("… rates not configured"). SHIF floors at KES 300. - Backfill/adjustment variants:
NssfManagementControllerhas backfill months +adjustStatutoryPaymentsForBackfill(), andCleanupController::backfillHelbPayrollPostings()retro-posts HELB — so historical months can be corrected outside the normal run. - GL split-by-branch:
postCreditByBranch/postDebitByBranchsplit each liability across branches (amountsByBranchFromColumn/amountsByBranchFromDeductions), so multi-branch tenants get per-branch GL lines.
7. Open Questions¶
Code-proven (facts established from source):
- Statutory (NSSF/PAYE/SHIF/Housing) are payslip columns; all other deductions are pivot rows keyed by Deduction.code. (PayrollCalculationService.)
- Driver/fuel/mileage/short-banking/late-shift penalties do reach payroll, via polymorphic EmployeePenalty rows created is_approved=0, requiring a second HR approval + monthly_pay_amount. (FuelEntryApprovalController:445, InboundDeliveryController:1855, PosBankingController:7899, EmployeePenaltyRequestController:97.)
- Balances are computed from ledgers, not stored, for penalties/HELB/benevolent. (model accessors.)
- Each statutory/scheme deduction credits a liability GL account and spawns a StatutoryPayment for remittance. (PostPayrollGLService:307–360, 413–515.)
Inference / not fully traced (needs confirmation):
- Route-level permission enforcement. The permission strings appear only in the nav Blade; I did not find middleware('permission:…') on the hr.php route groups. Whether the controllers hard-enforce these (vs. relying on nav hiding) is unconfirmed — should verify a checkPermission() call inside the controllers.
- Exact wa_gl_trans shape. GL writes go through the injected $this->gl->postPayrollCredit/Debit abstraction; I traced that a liability credit is posted but not the literal wa_gl_trans row columns (that's the Book 4 GL layer).
- Benevolent Fund has no balance cap in the calc (flat monthly_deduction); whether an upper limit/target exists elsewhere is unconfirmed.
- EmployeePenalty.employee_id semantics — code treats it as users.id (via User::where('employee_id',…)->pluck('id')), but the migration comments/relations are inconsistent (relation points to User, while EmployeeDeductionTransaction stores both employee_id and user_id). The dual-key handling is a likely source of subtle bugs; worth a dedicated audit.
- Medical Cover getPayrollDeductionAmount() internals (company-contribution split vs. employee premium) were not fully read.
- Whether NSSF's triple credit/debit posting in handleNSSF (two employee credits + one employer debit) is intentional double-counting or a compensating pattern — the two postCreditByBranch calls to the same account (also mirrored in handleHousingLevy) look suspicious and warrant review.
8. Source References¶
Nav / routes
- resources/views/admin/includes/sidebar_includes/hr.blade.php — permission strings, menu structure.
- routes/modules/hr.php:229–270 (advances, penalties, deduction balances), :443–455 (NSSF), :458–469 (HELB), :473–487 (Medical Cover), :489–498 (Benevolent), :508–518 (statutory reports incl. PAYE/SHIF/NSSF/Housing Levy).
Controllers
- app/Http/Controllers/Admin/HrSalaryAdvanceController.php — advances, penalties, deduction balances (penalty_Approval, waivePenalty, updatePenaltyMonthlyAmounts, "Balance brought forward" at :1605–1633).
- app/Http/Controllers/Admin/NssfManagementController.php — employeeStatus, remittance (:126), opt-out, backfill (:822, :869–975).
- app/Http/Controllers/Admin/HelbManagementController.php, MedicalCoverController.php, BenevolentFundManagementController.php.
- app/Http/Controllers/FuelEntryApprovalController.php:445–457 — driver fuel penalty → EmployeePenalty.
- app/Http/Controllers/Admin/InboundDeliveryController.php:1855–1872 — mileage penalty → EmployeePenalty.
- app/Http/Controllers/PosBankingController.php:7899, EmployeePenaltyRequestController.php:97,143, Admin/ShiftController.php:1780, CleanupController.php:3494.
- app/Http/Controllers/Admin/PayrollMonthController.php:197–391, 448–502, 1993–2020 — reads deductions back into payslip UI.
Services (engines)
- app/Services/PayrollCalculationService.php — calculateSystemDeductions() (:200–504), statutory calc (:60–150), user-defined 3-tier (:509–559).
- app/Services/PostPayrollService.php — processSalaryAdvances (:287–330), processPenalties (:339–383), processLoanRecoveries (:390–472), processBenevolent (:478–514), processHelb (:519–567), processMedicalCover (:572–…).
- app/Services/PostPayrollGLService.php — postCreditByBranch/postDebitByBranch (:125–167), handlePaye/SHIF/NSSF/HousingLevy/Helb (:413–515), createXStatutoryPayment (:307–360).
- app/Services/ProcessStatutoryVoucherService.php, SalaryAdvanceDirectPaymentService.php.
Models
- app/Models/SalaryAdvance.php (:194–208 balance), SalaryAdvancePayment.php, EmployeePenalty.php (:36 morphTo, :66–75 outstanding), EmployeeDeductionTransaction.php, EmployeeDeductionBalance.php, Nssf.php, EmployeeHelb.php (:100–130), EmployeeHelbTransaction.php, BenevolentFund.php, EmployeeBenevolentFund.php, MedicalCoverPlan.php, MedicalCoverEnrollment.php (:132), MedicalCoverTransaction.php, PayrollMonthDetailDeduction.php, Deduction.php, DeductionEmployeeSetting.php, DeductionBranchSetting.php, PayrollGlMapping.php, StatutoryPayment.php.
Migrations
- 2024_10_11_102640_create_payroll_month_detail_deductions_table.php + 2025_12_20…sacco_loan_id, 2026_05_19_100500…salary_advance_id, 2026_05_19_100600…employee_penalty_id, 2026_02_25…recovery_obligations_data, 2026_09_03…balance_snapshot.
- 2025_06_11…create_employee_penalties_table.php (+ 2025_06_18…add_is_approved, …penalty_id nullable, 2025_07_12…add_columns, 2025_08_11…waive_reason).
- 2025_06_12…salary_advances, …salary_advance_payments, …employee_deduction_balances, …employee_deduction_transactions.
- 2026_02_27…employee_helb(_transactions), 2026_03_24…medical_cover_(plans|enrollments|transactions), 2025_09_18/2026_04_08…benevolent_fund, 2025_10_09…statutory_payments(_items), 2025_10_08…payroll_gl_mappings, 2026_02_24…nssf_opt_out_fields.