P-08 · Upload payroll output (optional)
SOP:
Payroll_Processing.md§6 / Step 6.0 — output captureActors: Payroll Executive (PE). Pre-state: Cycle inEXPORTEDorCLIENT_REVIEW. Post-state:OutputBatchrow + parsedOutputRowrows + regenerated Spade-format approval report (now backed by real CPF / SDL figures).
Output upload is no longer a precondition for client approval. It exists to refresh the approval report with the real CPF / SDL figures from Infotech.
0. Prerequisites
- Cycle reached
EXPORTEDorCLIENT_REVIEW(P-07). - The client has an active OUTPUT
ExportTemplateVersion(or the system falls back toINFOTECH_DEFAULTfrompackages/excel/src/templates/index.ts). - A processed Infotech output workbook is available on disk for upload.
1. Steps
1.1 Open Upload output dialog
On /dashboard/cycles/<id> → Outputs tab, click Upload output. The button is visible while the cycle is EXPORTED or CLIENT_REVIEW.
1.2 Upload via standard presign / PUT / finalize
The dialog reuses the same 3-call ceremony as portal uploads:
POST /ops/files/presign
{ "originalName": "ACME-2026-04-output.xlsx", "fileKind": "IMPORTED_OUTPUT", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "sizeBytes": 84102 }
PUT <presigned MinIO URL> ...
POST /ops/files/<fileId>/finalize
{ "sha256": "<hex>", "sizeBytes": 84102 }1.3 Register the output batch
The dialog auto-calls:
POST /ops/cycles/<id>/outputs
{ "fileId": "<fileId>" }The route resolves the active OUTPUT (then UPLOAD) template for the client — you don't have to pass a templateVersionId from the UI anymore.
Server:
- Creates the
OutputBatchrow inparseStatus = PENDING. - Cycle transitions:
- From
EXPORTED→OUTPUT_IMPORTED. - From
CLIENT_REVIEW→ stays atCLIENT_REVIEW(the approval round is already in flight). The regenerated Spade-format report is picked up via latest-file-wins on the portal. No second approval request is sent.
- From
- Enqueues
output-parse.
1.4 Worker parses the output
The output-parse handler:
- Downloads the file from S3.
- Parses the workbook via the active OUTPUT template (or
INFOTECH_DEFAULTfallback). - Writes
OutputRowrows +EmployeeShadowSnapshotupdates. - Re-renders the Spade-format approval report from the parsed rows using
renderSpadeApprovalReport(parsedRows). - Uploads the new report to
<clientCode>/<cycleMonth>/approval-reports/v<n>/spade-report.xlsx— incrementing the version. The portal always picks the latestAPPROVAL_REPORTfile viafileRepo.findLatestByKind(), so the approver sees the refreshed CPF / SDL figures without needing a fresh email. - Marks the
OutputBatchasPARSED(orPARSED_WITH_ERRORSif any row failed).
2. Verification
Database
SELECT id, version_no, parse_status, row_count, error_count, parsed_at
FROM output_batches WHERE cycle_id = '<cycleId>'
ORDER BY version_no DESC LIMIT 1;
SELECT employee_ref, gross, net, cpf_employer, cpf_employee, sdl
FROM output_rows WHERE output_batch_id = '<outputId>'
ORDER BY employee_ref LIMIT 5;S3 / MinIO
<clientCode>/<cycleMonth>/outputs/v1/imported.xlsx— raw upload.<clientCode>/<cycleMonth>/approval-reports/v2/spade-report.xlsx— regenerated report (note the version increment).
Outputs tab
Within ~10 s the row's status flips PENDING → PARSED (or PARSED_WITH_ERRORS). The tab shows:
- Row count, error count, parsed timestamp.
- A Review report button per batch — opens the Spade-format xlsx.
Reference fixture
Compare against docs/output-sample-docs/Spade_March 2026 - Report_Spade format.xlsx — sheet header, employee rows, sub-total, CPF e-submission block.
3. Negative & edge cases
- S3 fetch failure →
OutputBatch.parseStatus = PARSE_FAILED. pg-boss retries with exponential backoff. - Parser errors (e.g. unexpected column header) →
parseStatus = PARSED_WITH_ERRORS. Rows that did parse are still persisted, and the report is still generated from whatever rows parsed. Errors are surfaced inerrors_json. - Report render failure (rare) → logged but does not fail the parse. PE re-imports the output to retry the render.
- Cycle in
APPROVEDor later → 400"Output upload is not allowed past the approval gate." - Re-uploading the same file (same SHA-256) → creates a new
OutputBatchwithversion_no = 2. We do not dedupe; a re-import is treated as an explicit re-render request.
Next
If you came from EXPORTED (no auto-approval), proceed to P-09 · Request approval. Otherwise the cycle is already in CLIENT_REVIEW — proceed to P-10 · Client approval decision.