P-04 · Validation & completeness
SOP:
Payroll_Processing.md§6 / Step 3.0 (S4 → S7)Actors: APOE (system). Triggered automatically as the first step ofGenerate Exportand on every file finalize while a cycle is inACTION_REQUIRED. Can also be re-run manually viaPOST /ops/cycles/:id/validate. Pre-state: Cycle inSUBMITTED,ACTION_REQUIRED, orREVISION_REQUESTED. Post-state: Cycle transitions toREADY_FOR_INTERNAL_REVIEW(no blocking failures) orACTION_REQUIRED(one or more blocking failures).
0. Prerequisites
1. Steps
The validation pipeline lives in packages/domain/src/rules/validation-rules.ts and is invoked by the validation-run worker handler.
1.1 Trigger via Generate Export (recommended)
This is the canonical path. See P-07. Validation is the first step inside the Generate Export click; a blocking failure aborts the export and flips the cycle to ACTION_REQUIRED.
1.2 Manual API trigger
POST /ops/cycles/<cycleId>/validate
Authorization: Bearer <staff-jwt>The route enqueues a validation-run pg-boss job and returns 202. The handler:
- Loads the cycle, latest submission, and all files.
- Builds a
ValidationContext. - Evaluates the 5 submission rules (
SG_PAY_PACK,packages/rules). How each is judged is in Validation rules → How the month's checks are judged:ATTENDANCE_REQUIRED— WARNING (not mandatory since 5 Oct 2026).DOCUMENT_REQUIREMENTS_MET— BLOCKING. Each change on its own: a document of its own, a person's approval, or the client's own typed entry. One issue per change that has none.FOREIGN_LEAVER_IR21— WARNING. The leaver's final pay is withheld until IRAS's clearance letter is recorded; there is no override.DUPLICATE_RECEIPTS— BLOCKING, Lead only. One issue per repeated claim line or file, against earlier claims that were paid.OCR_LOW_CONFIDENCE— WARNING, Lead only. One issue per document, while the change it rests on still awaits approval.
- Persists a
ValidationRunrow + per-ruleValidationResultrows. - For each blocking failure, ensures a corresponding
WorkflowIssueexists (idempotent — repeated runs do not create duplicate issues). - Determines the next status:
- All blocking pass →
READY_FOR_INTERNAL_REVIEW(orREADY_FOR_EXPORTif internal review already cleared). - Any blocking fails →
ACTION_REQUIRED.
- All blocking pass →
- Computes a completeness percentage
(passed_rules / total_rules) * 100.
1.3 Auto-revalidate after upload (singleton)
When the cycle is ACTION_REQUIRED, every POST /portal/files/<id>/finalize enqueues a validation-run job with singletonKey = "validation-run-<cycleId>-action-required". pg-boss collapses a burst of uploads into a single job, so a client uploading three files in quick succession triggers exactly one re-run.
2. Verification
Database
SELECT id, status, completeness_pct, ran_at
FROM validation_runs WHERE cycle_id = '<cycleId>'
ORDER BY ran_at DESC LIMIT 1;
SELECT rule_code, severity, outcome, message
FROM validation_results WHERE run_id = '<runId>';
SELECT id, rule_code, severity, status, opened_at
FROM workflow_issues WHERE cycle_id = '<cycleId>' AND status = 'OPEN';Worker logs
Validation run started cycleId=<id>
Validation run completed cycleId=<id> failedBlocking=2 newStatus=ACTION_REQUIREDWeb UI
/dashboard/cycles/<id> →
- Issues tab lists every open
WorkflowIssue. - Header shows the new status badge (
Action requiredorReady for internal review). - Completeness gauge updates.
3. Negative & edge cases
- No submission yet → 400
"Cycle has no submission to validate." - Cycle already in
EXPORTEDor later → 400"Validation cannot run after export."Use a revision round instead (P-10 revision branch). - Override an issue —
POST /ops/issues/:id/resolvewith{ "override": true, "reasonCode": "..." }. RequiresPAYROLL_LEAD. Closes the issue asOVERRIDDENand writes anissue.overriddenaudit event. The override covers that one finding (its claim line, document or change); a new one raises a fresh issue. - The IR21 warning cannot be overridden or resolved by hand (409): it closes when IRAS's clearance is recorded and the month is re-checked.
Next
- All blocking pass → P-06 · Internal review → P-07 · Generate export.
- Blocking failed → P-05 · Ask the client to put the gaps to the client (Ask client on the check).