ZyncDocs · MY-Payroll · Modules · Pay run lifecycle (detailed)
Must Should Could
Detailed Functional Requirements

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.

5
Statuses
9
Requirements
4
Period inputs

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.

Payroll run header
FieldTypeNotes
namestringRequired. Run label, e.g. "June 2026".
payDate keydateRequired. Its calendar month/year anchors proration, the MTD month, and the tax year.
fromDate, toDatedatesPay-period window; defaults to the calendar month of the pay date.
statusenumDRAFT / PENDING_APPROVAL / APPROVED / PAID / CANCELLED.
totalGrossSalary … totalTaxDeduction calcnumbersRoll-ups written by calculate: gross, net, employee contributions (statutory + tax), employer contributions, employer cost, HRDF levy, tax.
journalEntryIdreferenceSet by journal posting; required before mark-as-paid; cleared by every recalculation.
journalLiabilityAccountIdreferenceThe net-salary payable account chosen at post time.
calculatedAttimestampLast successful calculate.
PayrollEmployee per-employee line
FieldTypeNotes
employeeId, payrollIdreferencesOne line per employee per run.
salarynumberBasic salary snapshot at add time (from basic-salary item assignments).
totalUnpaidLeaveAmount inputnumberDeduction from approved unpaid leave in the period.
totalOvertimeAmount inputnumberOvertime pay from approved timesheets.
totalAbsenceDeductionAmount inputnumberDeduction for unexcused absent working days.
leaveIds[]referencesTraceability — the unpaid leaves folded into the deduction.
grossSalary, totalAllowances, totalDeductions, netSalary, employerCost, hrdfLevy, taxDeduction, taxRelief, taxableIncome calcnumbersWritten by calculate.
isCalculatedbooleantrue after a successful calculate; reset on reject. Uncalculated lines display live estimates.
taxYearnumberThe 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.

ActionAllowed fromResult
CreateNew empty run in DRAFT.
Edit header / add-remove employeesDRAFTUnchanged status.
Calculate (run)any except PAID / CANCELLED; needs ≥ 1 employeePENDING_APPROVAL; journal link cleared.
ApprovePENDING_APPROVALAPPROVED; notification to creator + managers.
RejectPENDING_APPROVALDRAFT; every line's calculated figures reset.
Post journalAPPROVEDJournal entry created and linked; status unchanged.
Mark as paidAPPROVED with a posted journalPAID (terminal); notification.
Cancelany except PAIDCANCELLED (terminal).
FR-RUN-L01Must
The system SHALL enforce the status transitions above, rejecting any action invoked from a disallowed state with a descriptive error.
Acceptance criteria
  • 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.
FR-RUN-L02Must
Rejecting a run SHALL return it to DRAFT and zero every line's calculated figures so stale numbers are never displayed as final.
Acceptance criteria
  • After reject, all lines report isCalculated = false and 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).

FR-RUN-E01Must
When employees are added to a run, the system SHALL snapshot, per employee and inside one transaction: the basic salary (from active basic-salary assignments), the unpaid-leave deduction, the overtime amount, the absence deduction, and the statutory contribution rows.
Acceptance criteria
  • 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).
FR-RUN-E02Must
The system SHALL provide a resync action that recomputes every line's period inputs and rebuilds its contributions — required after changing item assignments, leave, attendance or contribution groups — and SHALL allow manual per-line overrides of salary, overtime, unpaid-leave and absence amounts while the run is in DRAFT.
Acceptance criteria
  • 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.
FR-RUN-E03Should
Before calculation, uncalculated lines SHALL display live estimated figures (gross, net, tax) so a DRAFT run shows projected totals; after calculation the stored figures are returned verbatim.
Acceptance criteria
  • 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.

FR-RUN-P01Must
When an employee joins after the 1st of the pay month or resigns within it, the system SHALL prorate by 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.
Acceptance criteria
  • 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

#StepRule
1Split itemsActive assignments split into normal vs additional remuneration.
2Additional grossΣ additional items — full lump sum.
3Base + allowancesProrated basic salary and prorated non-basic allowances; prorated item deductions.
4Normal grossbase + allowances + overtime − unpaidLeave − absence (rounded).
5Statutory sumsEmployee deductions = Σ employee shares; employer contributions = Σ employer shares (from stored contribution rows).
6ReliefsAnnualise full-month statutory amounts ×12, cap per scheme; resolve annual personal relief and residency.
7PCB / MTDCompute tax, relief and chargeable income (see PCB / MTD), zakat offsets applied.
8Paid grossnormal gross + additional gross.
9Netmax(0, paidGross − itemDeductions − employeeStatutory − tax); total deductions = paid gross − net.
10HRDFEmployer levy on prorated base + allowances (Malaysian employees, when enabled).
11Employer costpaidGross + employerContributions + hrdfLevy.
FR-RUN-X01Must
The system SHALL execute the calculation exactly in the order above and write every line plus the run roll-ups (gross, net, employee contributions incl. tax, employer contributions, employer cost, HRDF, tax) in a single atomic transaction, stamping the calculation time and clearing any journal link.
Acceptance criteria
  • 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.

FR-RUN-W01Must
Approving a calculated run SHALL lock its figures; marking it paid SHALL require a posted journal; both actions (and reject) SHALL notify the run's creator and company managers in-app.
Acceptance criteria
  • 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.
FR-RUN-W02Could
The system MAY add maker/checker separation — today any user with HR access can create, calculate and approve the same run; approval discipline is procedural, not enforced.
Acceptance criteria
  • 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

Known gaps

Re-running an approved, journalled run orphans the GL entry. Recalculation clears the run's journal link but does not delete or reverse the posted journal entry — re-posting later creates a second entry unless the first is manually removed.
Delete has no status guard. The delete mutation cascades (lines + contributions) regardless of status — a PAID run can be deleted; only the UI's advisory can-delete flag (DRAFT-only) protects it.
No maker/checker. The same user can calculate and approve; no per-action permission distinguishes preparer from approver.
Claims, loans and advances are not fed into the run. Reimbursements and instalment deductions must be entered manually as item assignments — there is no automated integration with those HR modules.
Mixed date semantics. Proration uses local-time date parts while the statutory schedule and imports use UTC; cross-timezone deployments risk off-by-one proration.
Cancel does not clean up. Cancelling neither reverses a posted journal nor resets line figures.