Skip to content

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 monthly retention-purge job. Pre-state: Cycle in APPROVED. Post-state: Cycle transitions APPROVED → FINALIZED → CLOSED (manual). Eventual CLOSED → 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.

http
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.

http
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 ​

sql
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.closed

Client 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 refuse 409 HeldFinalPayNotAcknowledged while final pay is withheld and the exclusion is not acknowledged, and record payroll.payment.final_pay_withheld when it is.
  • POST /ops/cycles/:id/cpf-sign-off/undo and POST /ops/cycles/:id/evidence/:evidenceId/remove take { reason } (at least 10 characters), kept on the audit row.
  • POST /ops/cycles/:id/archive is 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, an IR21_ACKNOWLEDGEMENT evidence row or { ir21Reason } (400 IR21_ACKNOWLEDGEMENT_MISSING otherwise). POST /ops/cycles/:id/archive-without-bank-confirmation takes { 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-pack returns 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-figures draft and send Infotech's final figures to the client (staff-sent; see the how-to).

3. Negative & edge cases ​

  • Finalize on a non-APPROVED cycle → 400 with the actual current status.
  • Close on a non-FINALIZED cycle → 400.
  • Archive a non-CLOSED cycle 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 a RETRO_ADJUSTMENT change type (handled via the next cycle's intake).

Done ​

The cycle has run end-to-end.

Internal use only — BreezyCorp