Skip to content

P-10 · Client approval decision

SOP: Payroll_Processing.md §6 / Step 6.0 (S10 → S14)Actors: Client Approver (CR with canApprove = true). Pre-state: Cycle in CLIENT_REVIEW, ApprovalRound open. Approver has the magic-link email. Post-state: Cycle transitions to APPROVED, REVISION_REQUESTED, or CLIENT_QUERY_OPEN depending on decision.

0. Prerequisites

  • Either P-07 auto-fired or P-09 manually fired the approval request.
  • Approver has the email in mailpit.
  • The seed's GLOBEX client is pre-positioned in this state — bob@globex.sg has the magic link.

1. Steps

In an incognito window, click Approve payroll in the email. Browser lands on http://localhost:3000/portal/<jwt>.

The portal:

  1. Verifies the SHA-256 token hash against cycle_requests.
  2. Resolves the cycle. Because cycle status is CLIENT_REVIEW, the same [token]/page.tsx routes to the approval view (no separate approval route exists).
  3. Renders a Payroll report — {month} download card:
    • Filename, version, generated timestamp.
    • Download button → GET /portal/files/<reportFileId>/download (Bearer auth) → presigned S3 URL.
    • The portal looks up the freshest APPROVAL_REPORT file via fileRepo.findLatestByKind(), so it picks up regenerated reports from P-08 automatically.
    • If no APPROVAL_REPORT file exists at all (defensive), the card falls back to a "Report not ready" amber banner instead of crashing.

Open the downloaded xlsx and confirm:

  • Company header, monthly summary title, sub-header rows.
  • Employee rows with codes, names, basic salary.
  • Sub-total row.
  • CPF e-submission summary block.
  • If you took P-07 without P-08 (path A): CPF / SDL / NetWages columns are 0.
  • If you took P-08 (path B): CPF / SDL / NetWages reflect the processor's figures.

1.2 Make the decision

10a. Happy path — APPROVED

Click Approve, optionally add a note, submit:

http
POST /portal/approval/<token>/respond
{ "decision": "APPROVED", "note": "Looks good, proceed." }

Cycle transitions CLIENT_REVIEW → APPROVED. The ApprovalRound row stamps decided_at, decision = 'APPROVED', note.

10b. Revision path

Click Request Revision, explain why, submit:

http
POST /portal/approval/<token>/respond
{ "decision": "REVISION_REQUESTED", "note": "Marcus's salary should be 5500, not 5000." }

Cycle transitions CLIENT_REVIEW → REVISION_REQUESTED. On the next portal visit by a canSubmit = true contact, the cycle auto-flips to INTAKE_IN_PROGRESS (closing the loop for a re-submit). A new Submission is created on submit, with version_no = 2. After re-submit + re-validate + re-export, a new ApprovalRound is opened (round 2), and the approver receives a fresh email.

10c. Query path

Click Raise Query, add the question, submit:

http
POST /portal/approval/<token>/respond
{ "decision": "QUERY", "note": "Is the new joiner's CPF set up correctly?" }

Cycle transitions CLIENT_REVIEW → CLIENT_QUERY_OPEN. Ops responds via the cycle detail page's Approval messages tab; that response transitions back to CLIENT_REVIEW so the approver can re-decide.

2. Verification

Database

sql
SELECT round_no, decision, decided_at, note
  FROM approval_rounds WHERE cycle_id = '<cycleId>'
  ORDER BY round_no DESC LIMIT 1;

SELECT status FROM payroll_cycles WHERE id = '<cycleId>';
-- 'APPROVED', 'REVISION_REQUESTED', or 'CLIENT_QUERY_OPEN'

Audit log

DecisionAudit event
APPROVEDapproval.responded (decision = APPROVED) → cycle.approved
REVISIONapproval.responded (decision = REVISION_REQUESTED) → cycle.revision_requested
QUERYapproval.responded (decision = QUERY) → cycle.client_query_open

Mailpit

A confirmation email lands in the PE inbox (or whatever address is configured for ops notifications) summarising the decision. The approver does not receive a confirmation email by default.

3. Negative & edge cases

  • Token expired → 401 on the portal load. The cycle stays at CLIENT_REVIEW. Use P-09 again, picking the same approver, to mint a fresh token.
  • Approver-only contact already responded → 409 with the existing decision. Decisions are terminal per round; a revision opens a new round.
  • Cycle in APPROVED, CLOSED, or FINALIZED → 409 "Approval has already concluded for this round."
  • Submitter-only contact opens the link → portal renders the read-only "Submitted" view, never the approval card.
  • Empty note on REVISION → 400 "A revision reason is required."

Next

  • APPROVED → P-11 · Finalize, close, archive.
  • REVISION → P-02 again (cycle is back at INTAKE_IN_PROGRESS), then re-validate, re-export, re-request approval.
  • QUERY → respond from ops, then approver re-decides.

Internal use only — BreezyCorp