P-11 · Finalize, close, archive
SOP:
Payroll_Processing.md§6 / Step 7.0 (S14 → S16)Actors: Payroll Executive (PE) for Finalize and Close. The Archive transition is automatic via the monthlyretention-purgejob. Pre-state: Cycle inAPPROVED. Post-state: Cycle transitionsAPPROVED → FINALIZED → CLOSED(manual). EventualCLOSED → ARCHIVED(cron).
0. Prerequisites
- Client has approved (P-10, happy path).
1. Steps
1.1 Capture post-payroll evidence (optional but recommended)
Per SOP §6 / Step 7.0, attach evidence files before finalizing:
- Bank disbursement confirmation
- CPF submission acknowledgement
- IR21 acknowledgement (if any foreign leavers)
Upload via the cycle's Files tab (fileKind = EVIDENCE). They are referenced from the audit log but do not block the transition.
1.2 Finalize
On /dashboard/cycles/<id>, click Finalize.
POST /ops/cycles/<id>/finalize
Authorization: Bearer <staff-jwt>Cycle transitions APPROVED → FINALIZED. Audit event cycle.finalized. From this state, no further submission, export, or approval is allowed.
1.3 Close
Click Close.
POST /ops/cycles/<id>/close
Authorization: Bearer <staff-jwt>Cycle transitions FINALIZED → CLOSED. Audit event cycle.closed. The cycle now appears in archived listings on /dashboard/cycles?status=CLOSED.
1.4 Archive (automatic)
The retention-purge job runs at 0 3 1 * * UTC (03:00 on the 1st of each month). It transitions every CLOSED cycle older than the configured retention window (default: keep CLOSED for 12 months, then ARCHIVED). ARCHIVED cycles surface only under explicit filter.
This is fully automatic — do not block manual testing on it.
2. Verification
Database
SELECT status, finalized_at, closed_at, archived_at FROM payroll_cycles WHERE id = '<cycleId>';Audit log (/dashboard/cycles/<id>/audit)
The full lifecycle is visible end-to-end:
cycle.created
cycle.requested
cycle.intake_started
file.finalized × N
ocr.completed × N
submission.submitted
cycle.submitted
validation.run.completed
issue.* (any resolutions / overrides)
cycle.ready_for_internal_review
cycle.exported
approval_report.generated
approval.requested
approval.responded
cycle.approved
cycle.finalized
cycle.closedClient message
When the month closes on approval of the payment instruction (CPF_PENDING → CLOSED), the client is sent "Your {Month YYYY} payroll is done. Salaries go out on {date}. Your employees will get their payslips from Infotech." once per month, by email to each payroll sender and to the approver (the date only when a payment date is recorded). No "payslips are ready" message is sent.
Current behaviour (28 Oct 2026)
- The payment gate (
CPF_PENDING → CLOSED) is a Payroll Lead's on both kinds of month: the Approve payment card of a run month and Approve payment and close (POST /ops/cycles/:id/close, body{ acknowledgeHeldFinalPay }) of a month with no run. Both confirm first ("cannot be undone", withheld leavers named); both refuse409 HeldFinalPayNotAcknowledgedwhile final pay is withheld and the exclusion is not acknowledged, and recordpayroll.payment.final_pay_withheldwhen it is. POST /ops/cycles/:id/cpf-sign-off/undoandPOST /ops/cycles/:id/evidence/:evidenceId/removetake{ reason }(at least 10 characters), kept on the audit row.POST /ops/cycles/:id/archiveis Payroll Lead only (with Admin / Platform Admin; no not-the-preparer rule); it needs the bank confirmation and, where a final-pay hold exists on the month, anIR21_ACKNOWLEDGEMENTevidence row or{ ir21Reason }(400 IR21_ACKNOWLEDGEMENT_MISSINGotherwise).POST /ops/cycles/:id/archive-without-bank-confirmationtakes{ reason, ir21Reason? }and applies the same IR21 rule; the console asks for both reasons in one box when the acknowledgement is owed.GET /ops/cycles/:id/evidence-packreturns the evidence zip.- A late document on a finalized month becomes a correction card on the next month only while that month is still collecting inputs; when it opens, late documents from every finalized earlier month are carried, not only the latest. If the next month is already past intake, the warning on the finalized month and the email to the Leads and preparers say to add the correction there yourself or decide a separate payment, and the warning stays open.
GET /ops/cycles/:id/final-figures/draft/POST /ops/cycles/:id/final-figuresdraft and send Infotech's final figures to the client (staff-sent; see the how-to).
3. Negative & edge cases
- Finalize on a non-
APPROVEDcycle → 400 with the actual current status. - Close on a non-
FINALIZEDcycle → 400. - Archive a non-
CLOSEDcycle manually → no manual archive endpoint; the only path is the cron. - Re-open a closed cycle — there is no re-open API. Instead, open a brand-new cycle for the same month? No — the unique constraint
(client_id, cycle_month)blocks that. For correction-after-close, you handle it in the next month's cycle as aRETRO_ADJUSTMENTchange type (handled via the next cycle's intake).
Done
The cycle has run end-to-end.