Skip to content

P-04 · Validation & completeness ​

SOP: Payroll_Processing.md §6 / Step 3.0 (S4 → S7)Actors: APOE (system). Triggered automatically as the first step of Generate Export and on every file finalize while a cycle is in ACTION_REQUIRED. Can also be re-run manually via POST /ops/cycles/:id/validate. Pre-state: Cycle in SUBMITTED, ACTION_REQUIRED, or REVISION_REQUESTED. Post-state: Cycle transitions to READY_FOR_INTERNAL_REVIEW (no blocking failures) or ACTION_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.

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 ​

http
POST /ops/cycles/<cycleId>/validate
Authorization: Bearer <staff-jwt>

The route enqueues a validation-run pg-boss job and returns 202. The handler:

  1. Loads the cycle, latest submission, and all files.
  2. Builds a ValidationContext.
  3. 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.
  4. Persists a ValidationRun row + per-rule ValidationResult rows.
  5. For each blocking failure, ensures a corresponding WorkflowIssue exists (idempotent — repeated runs do not create duplicate issues).
  6. Determines the next status:
    • All blocking pass → READY_FOR_INTERNAL_REVIEW (or READY_FOR_EXPORT if internal review already cleared).
    • Any blocking fails → ACTION_REQUIRED.
  7. 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 ​

sql
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_REQUIRED

Web UI ​

/dashboard/cycles/<id> →

  • Issues tab lists every open WorkflowIssue.
  • Header shows the new status badge (Action required or Ready for internal review).
  • Completeness gauge updates.

3. Negative & edge cases ​

  • No submission yet → 400 "Cycle has no submission to validate."
  • Cycle already in EXPORTED or later → 400 "Validation cannot run after export." Use a revision round instead (P-10 revision branch).
  • Override an issue — POST /ops/issues/:id/resolve with { "override": true, "reasonCode": "..." }. Requires PAYROLL_LEAD. Closes the issue as OVERRIDDEN and writes an issue.overridden audit 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 ​

Internal use only — BreezyCorp