P-10 · Client approval decision
SOP:
Payroll_Processing.md§6 / Step 6.0 (S10 → S14)Actors: Client Approver (CR withcanApprove = true). Pre-state: Cycle inCLIENT_REVIEW,ApprovalRoundopen. Approver has the magic-link email. Post-state: Cycle transitions toAPPROVED,REVISION_REQUESTED, orCLIENT_QUERY_OPENdepending 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.sghas the magic link.
1. Steps
1.1 Open the approval link
In an incognito window, click Approve payroll in the email. Browser lands on http://localhost:3000/portal/<jwt>.
The portal:
- Verifies the SHA-256 token hash against
cycle_requests. - Resolves the cycle. Because cycle status is
CLIENT_REVIEW, the same[token]/page.tsxroutes to the approval view (no separate approval route exists). - 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_REPORTfile viafileRepo.findLatestByKind(), so it picks up regenerated reports from P-08 automatically. - If no
APPROVAL_REPORTfile 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:
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:
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:
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
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
| Decision | Audit event |
|---|---|
| APPROVED | approval.responded (decision = APPROVED) → cycle.approved |
| REVISION | approval.responded (decision = REVISION_REQUESTED) → cycle.revision_requested |
| QUERY | approval.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, orFINALIZED→ 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.