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 core rules:
    • ATTENDANCE_REQUIRED — BLOCKING. At least one ATTENDANCE-kind file or an explicit "no attendance changes" declaration field must be present.
    • DOCUMENT_REQUIREMENTS_MET — BLOCKING. Every active DocumentRequirementRule for the change types in the submission must be satisfied (e.g. JOINER → OFFER_LETTER + IDENTITY_NRIC + BANK_DETAILS).
    • FOREIGN_LEAVER_IR21 — BLOCKING. Any LEAVER item where the employee is foreign (FIN holder) requires an IR21-kind file or an authorized override.
    • NS_DOCUMENTATION — BLOCKING. NS claim items require accompanying NS documentation.
    • DUPLICATE_RECEIPTS — WARNING. Duplicate SHA-256 hashes across receipts in the cycle. Override allowed by PAYROLL_LEAD.
  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 issuePOST /ops/issues/:id/resolve with { "override": true, "reason": "..." }. Requires PAYROLL_LEAD. Writes a WorkflowIssueOverride row + issue.overridden audit event.
  • Duplicate receipts override — same as above; the WARNING does not block by itself, but if business rules upgrade it to BLOCKING (configurable per client), use override.

Next

Internal use only — BreezyCorp