Pay Run Lifecycle — Detailed Requirements
The monthly pay run: a container of per-employee pay lines moving through DRAFT → PENDING_APPROVAL → APPROVED → PAID (or CANCELLED). Adding an employee snapshots their period inputs — unpaid leave, overtime, absence, and statutory contribution rows. Calculate reads the standing item assignments live, prorates for partial months, applies statutory deductions and annualised PAYE, and writes net pay and employer cost atomically. Approval locks the figures; journal posting and payment close the run.
Lifecycle
Five-state machine; DRAFT is the only editable state, enforced as advisory flags plus transition guards.
Snapshotting
Leave/OT/absence and contributions frozen per employee at add/resync; items read live at calculate.
Calculation
Prorate → gross → statutory → annualised PAYE → net → employer cost, in one transaction.
Approval & payment
Approve locks; reject loops to DRAFT; paid requires a posted journal.
Pay Run · Detailed
Data model
The run header plus the per-employee line. The line has two layers: period inputs written at add/resync, 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 and selects the tax bracket year. |
fromDate, toDate | dates | Pay-period window; defaults to the pay date's calendar month. |
status | enum | DRAFT / PENDING_APPROVAL / APPROVED / PAID / CANCELLED. |
totalGrossSalary … totalTaxDeduction calc | numbers | Roll-ups written by calculate: gross, net, employee contributions (statutory + PAYE), employer contributions, employer cost, 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, totalDeductions, netSalary, employerCost, 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. |
ytdTaxDeducted | number | unused Present but never accumulated — PAYE has no year-to-date true-up. |
Pay Run · Detailed
Lifecycle & statuses RUN-L
Editability is status-driven, not role-driven: 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 (advisory) | 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); nothing cleaned up. |
- Calculating a paid or cancelled run, approving a draft, or marking an unjournalled run as paid all fail.
- Approve, reject and mark-as-paid notify the run's creator and company managers in-app (fire-and-forget).
DRAFT and zero every line's calculated figures in one transaction, so stale numbers are never displayed as final.- After reject, all lines report
isCalculated = falseand show live estimates again.
Pay Run · Detailed
Adding employees — snapshotting period inputs RUN-E
Adding an employee freezes their attendance-derived inputs and contribution rows. The working context is the employee's shift weekdays minus company holidays within the period (default Monday–Friday); the salary base for these inputs is gross (basic + allowances).
- Unpaid leave: approved unpaid leaves overlapping the period, clamped to it; half-days count 0.5; only working days count; amount = leave days × (gross ÷ working days in period).
- Overtime: approved timesheet hours × hourly rate, where hourly rate = gross ÷ working days per month ÷ hours per day (defaults 30 × 8 from the attendance group).
- Absence: (working days − days present) × daily rate; approved leaves — paid or unpaid — count as present so unpaid leave is never double-deducted.
- Contributions: prorated and full-month twin amounts stored per scheme (see statutory contributions).
- Resync overwrites manual overrides — operators re-apply overrides after a resync.
- Removing a line (or deleting the run) cascades to its contribution rows.
- Earning/deduction items are not snapshotted — the run reads active assignments live at calculate time; only attendance inputs and contributions freeze at add time.
- Estimates skip join/resign proration — a mid-month joiner's estimate can exceed the calculated result.
Pay Run · Detailed
Proration for partial months RUN-P
workedDays ÷ daysInMonth (worked days = join day, or the 1st, through the resign day, or month end).- Join on the 16th of a 30-day month → factor 15/30 = 0.5.
- Joining on the 1st, or in a prior month, is a full month.
- Overtime, unpaid leave and absence are computed per actual day and are not re-prorated.
Pay Run · Detailed
Calculate — order of operations RUN-X
Calculate computes every employee in a read phase — loading the pay year's tax bracket and each employee's tax profile — then writes all lines and the run totals in one transaction, moving the run to PENDING_APPROVAL.
| # | Step | Rule |
|---|---|---|
| 1 | Read items live | Active assignments at the pay date, annual amounts ÷12. |
| 2 | Base + allowances | Prorated basic salary, prorated non-basic allowances, prorated item deductions. |
| 3 | Gross | base + allowances + overtime − unpaidLeave − absence (rounded). |
| 4 | Statutory sums | Employee deductions = Σ employee shares; employer contributions = Σ employer shares (from stored contribution rows). |
| 5 | Reliefs | Annual statutory reliefs = full-month employee amounts ×12; personal relief from the bracket + employee profile; item tax reliefs. |
| 6 | PAYE | Annualised band walk (see PAYE) — no year-to-date true-up, no bonus method. |
| 7 | Net | max(0, gross − itemDeductions − employeeStatutory − tax); total deductions = gross − net. |
| 8 | Employer cost | gross + employerContributions (no levy schemes on this instance). |
- A failure anywhere leaves no partially-calculated run.
- Net pay floors at zero; the shortfall is absorbed silently into total deductions.
- All amounts round to 2 decimals at each step.
Pay Run · Detailed
Approval & payment RUN-W
- Mark-as-paid without a journal fails; marking paid records status only — the actual bank settlement is a separate finance action.
- No notification fires on calculate, cancel or journal posting, and employees themselves are never notified.
- Documented control gap; no per-action permission exists on run progression.
Pay Run · Detailed
Business rules
- Two data temperatures. Attendance inputs and contributions freeze at add/resync; items are read live at calculate — editing an assignment changes the next calculate without a resync, but contribution changes need one.
- Two divisors coexist: unpaid-leave/absence value days at gross ÷ actual working days in the period; overtime uses the flat 30-day × 8-hour divisor.
- Recalculation is allowed even from APPROVED (resets to PENDING_APPROVAL, clears the journal link) — only PAID and CANCELLED block it.
- The pay date anchors everything: its month drives proration and the period default; its year selects the tax bracket.
- Run header + line editability is DRAFT-only as an advisory flag; transition guards are the hard enforcement.
Pay Run · Detailed