Skip to content

P-08 · Upload payroll output (optional)

SOP: Payroll_Processing.md §6 / Step 6.0 — output captureActors: Payroll Executive (PE). Pre-state: Cycle in EXPORTED or CLIENT_REVIEW. Post-state: OutputBatch row + parsed OutputRow rows + 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 EXPORTED or CLIENT_REVIEW (P-07).
  • The client has an active OUTPUT ExportTemplateVersion (or the system falls back to INFOTECH_DEFAULT from packages/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:

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

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

  1. Creates the OutputBatch row in parseStatus = PENDING.
  2. Cycle transitions:
    • From EXPORTEDOUTPUT_IMPORTED.
    • From CLIENT_REVIEWstays at CLIENT_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.
  3. Enqueues output-parse.

1.4 Worker parses the output

The output-parse handler:

  1. Downloads the file from S3.
  2. Parses the workbook via the active OUTPUT template (or INFOTECH_DEFAULT fallback).
  3. Writes OutputRow rows + EmployeeShadowSnapshot updates.
  4. Re-renders the Spade-format approval report from the parsed rows using renderSpadeApprovalReport(parsedRows).
  5. Uploads the new report to <clientCode>/<cycleMonth>/approval-reports/v<n>/spade-report.xlsx — incrementing the version. The portal always picks the latest APPROVAL_REPORT file via fileRepo.findLatestByKind(), so the approver sees the refreshed CPF / SDL figures without needing a fresh email.
  6. Marks the OutputBatch as PARSED (or PARSED_WITH_ERRORS if any row failed).

2. Verification

Database

sql
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 PENDINGPARSED (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 failureOutputBatch.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 in errors_json.
  • Report render failure (rare) → logged but does not fail the parse. PE re-imports the output to retry the render.
  • Cycle in APPROVED or later → 400 "Output upload is not allowed past the approval gate."
  • Re-uploading the same file (same SHA-256) → creates a new OutputBatch with version_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.

Internal use only — BreezyCorp