Pay Run Lifecycle — Detailed Requirements
The monthly pay run is the heart of MY-Payroll: a container of per-employee pay lines that moves through DRAFT → PENDING_APPROVAL → APPROVED → PAID (or CANCELLED). Adding an employee snapshots their period inputs — overtime, unpaid leave, absence, statutory contributions. Calculate assembles gross pay from standing items, prorates for partial months, applies statutory deductions and PCB, and writes net pay and employer cost atomically. Approval locks the figures; the journal posting and payment close the run.
Lifecycle
Five-state machine with status-driven editability — DRAFT is the only editable state.
Snapshotting
OT, unpaid leave, absence and contributions frozen per employee when added or resynced.
Calculation
Prorate → gross → statutory → PCB/MTD → net → employer cost, all written in one transaction.
Approval & payment
Approve locks; reject loops back to DRAFT; paid requires a posted journal.
Pay Run · Detailed
Data model
Two entities: the run header and the per-employee line. The line has two layers — period inputs written at add/resync time, and calculated outputs written by calculate and gated by isCalculated.
run header| Field | Type | Notes |
|---|---|---|
name | string | Required. Run label, e.g. "June 2026". |
payDate key | date | Required. Its calendar month/year anchors proration, the MTD month, and the tax year. |
fromDate, toDate | dates | Pay-period window; defaults to the calendar month of the pay date. |
status | enum | DRAFT / PENDING_APPROVAL / APPROVED / PAID / CANCELLED. |
totalGrossSalary … totalTaxDeduction calc | numbers | Roll-ups written by calculate: gross, net, employee contributions (statutory + tax), employer contributions, employer cost, HRDF levy, tax. |
journalEntryId | reference | Set by journal posting; required before mark-as-paid; cleared by every recalculation. |
journalLiabilityAccountId | reference | The net-salary payable account chosen at post time. |
calculatedAt | timestamp | Last successful calculate. |
per-employee line| Field | Type | Notes |
|---|---|---|
employeeId, payrollId | references | One line per employee per run. |
salary | number | Basic salary snapshot at add time (from basic-salary item assignments). |
totalUnpaidLeaveAmount input | number | Deduction from approved unpaid leave in the period. |
totalOvertimeAmount input | number | Overtime pay from approved timesheets. |
totalAbsenceDeductionAmount input | number | Deduction for unexcused absent working days. |
leaveIds[] | references | Traceability — the unpaid leaves folded into the deduction. |
grossSalary, totalAllowances, totalDeductions, netSalary, employerCost, hrdfLevy, taxDeduction, taxRelief, taxableIncome calc | numbers | Written by calculate. |
isCalculated | boolean | true after a successful calculate; reset on reject. Uncalculated lines display live estimates. |
taxYear | number | The pay date's year at calculate time. |
Pay Run · Detailed
Lifecycle & statuses RUN-L
Editability is status-driven, not role-driven: DRAFT is the only editable state, and the status machine is the entire authorisation model for run progression.
| Action | Allowed from | Result |
|---|---|---|
| Create | — | New empty run in DRAFT. |
| Edit header / add-remove employees | DRAFT | Unchanged status. |
| Calculate (run) | any except PAID / CANCELLED; needs ≥ 1 employee | PENDING_APPROVAL; journal link cleared. |
| Approve | PENDING_APPROVAL | APPROVED; notification to creator + managers. |
| Reject | PENDING_APPROVAL | DRAFT; every line's calculated figures reset. |
| Post journal | APPROVED | Journal entry created and linked; status unchanged. |
| Mark as paid | APPROVED with a posted journal | PAID (terminal); notification. |
| Cancel | any except PAID | CANCELLED (terminal). |
- Calculating a paid or cancelled run, approving a draft, or marking an unjournalled run as paid all fail.
- Every status change is audited with a snapshot.
DRAFT and zero every line's calculated figures so stale numbers are never displayed as final.- After reject, all lines report
isCalculated = falseand show live estimates again. - The operator can amend items/employees and recalculate.
Pay Run · Detailed
Adding employees — snapshotting period inputs RUN-E
Adding an employee freezes their period inputs. The working context is the employee's shift weekdays minus company holidays within the pay period (default Monday–Friday).
- Unpaid leave: approved unpaid leaves overlapping the period, clamped to it; half-days count 0.5; non-working days skipped; amount = leave days × (salary ÷ working days).
- Overtime: approved timesheet hours in the period × hourly rate (salary ÷ working days per month ÷ hours per day).
- Absence: working days minus days present (attended dates plus any approved leave, paid or unpaid — so unpaid leave is never double-deducted) × daily rate.
- Contributions: one row per statutory type in the employee's contribution group (see statutory / STA-R).
- Contribution rows are deleted and recreated on resync.
- Removing a line (or the whole run) cascades to its contribution rows.
- Snapshots do not self-update: without a resync, a run calculated later still uses the amounts frozen at add time.
- Estimates use the simplified annualised tax path and skip proration — they may differ from the calculated result and are advisory only.
Pay Run · Detailed
Proration for partial months RUN-P
Mid-month joiners and leavers are paid by calendar-day fraction of the pay month.
workedDays ÷ daysInMonth, where worked days run from the join day (or the 1st) to the resign day (or month end), applied to: basic salary, allowances, item deductions, the statutory contribution base, and the HRDF base.- Join on the 16th of a 30-day month → factor 15/30 = 0.5.
- Joining on the 1st is a full month (no proration).
- Overtime, unpaid leave and absence are computed per actual day and are not re-prorated; additional remuneration (bonus) is never prorated.
Pay Run · Detailed
Calculate — order of operations RUN-X
Calculate computes every employee in a read phase, then writes all lines and the run totals in one transaction, moving the run to PENDING_APPROVAL.
Per-run setup
- Load the run and its lines (≥ 1 employee required); derive the pay month and tax year from the pay date.
- Load the year's tax bracket (bands, personal reliefs, statutory relief caps).
- Build the year-to-date accumulation per employee — gross, employee EPF, and tax from this year's earlier approved/paid runs — for the MTD true-up.
- Load the HRDF configuration.
Per employee
| # | Step | Rule |
|---|---|---|
| 1 | Split items | Active assignments split into normal vs additional remuneration. |
| 2 | Additional gross | Σ additional items — full lump sum. |
| 3 | Base + allowances | Prorated basic salary and prorated non-basic allowances; prorated item deductions. |
| 4 | Normal gross | base + allowances + overtime − unpaidLeave − absence (rounded). |
| 5 | Statutory sums | Employee deductions = Σ employee shares; employer contributions = Σ employer shares (from stored contribution rows). |
| 6 | Reliefs | Annualise full-month statutory amounts ×12, cap per scheme; resolve annual personal relief and residency. |
| 7 | PCB / MTD | Compute tax, relief and chargeable income (see PCB / MTD), zakat offsets applied. |
| 8 | Paid gross | normal gross + additional gross. |
| 9 | Net | max(0, paidGross − itemDeductions − employeeStatutory − tax); total deductions = paid gross − net. |
| 10 | HRDF | Employer levy on prorated base + allowances (Malaysian employees, when enabled). |
| 11 | Employer cost | paidGross + employerContributions + hrdfLevy. |
- A failure anywhere leaves no partially-calculated run.
- Net pay floors at zero — deductions can never produce negative pay; the shortfall stays in total deductions.
- All amounts round to 2 decimals except where a statutory rounding regime applies.
Pay Run · Detailed
Approval & payment RUN-W
A lightweight built-in approve/reject — payroll does not use the generic approval-policy engine, and there is no multi-level chain.
- Mark-as-paid without a journal fails with "post the payroll journal before marking as paid".
- No notification fires on calculate/submit, and employees themselves are not notified.
- Documented as a control gap; no per-action RBAC exists on run progression.
Pay Run · Detailed
Business rules
- Contributions are frozen at add/resync, not at calculate. Calculate reads the stored contribution rows; stale assignments require a resync first.
- Two proration bases coexist by design: salary/allowances prorate by calendar days; unpaid-leave and absence value days at salary ÷ working days. The effective daily value differs.
- Recalculation is allowed even from APPROVED (it resets to PENDING_APPROVAL and clears the journal link) — only PAID and CANCELLED block it.
- The pay date is the period anchor: its month drives proration and MTD; its year selects the tax bracket and scopes YTD.
- Every mutation is audited; all multi-document writes run in retry transactions.
Pay Run · Detailed