FS-08 · Client portal sign-off (optional)
SOP:
SOP_Financial_Statements.md§6 / Step 8.0Actors: FS Reviewer dispatches; Client Approver decides via the magic-link portal. Pre-state: Engagement atNOTES_COMPLETEwith at least one DOCX export available. Post-state:AuditEventcaptures the client decision. Engagement remains atNOTES_COMPLETE. On revision request, the preparer addresses upstream and re-sends.
0. Prerequisites
- Engagement at
NOTES_COMPLETEwith at least one DOCX export (see FS-07). - Client has at least one active contact with
canApprove = true.
1. Steps
1.1 Dispatch the portal link
POST /ops/financial-statements/engagements/<engagementId>/send-for-review
Content-Type: application/json
Authorization: Bearer <fs-reviewer-jwt>
{ "contactId": "<approverContactId>" }(Or click Send to client for review on the engagement detail page and pick the contact.)
The handler:
- Asserts
MANAGE_FS_ENGAGEMENT. - Creates a
PortalInvitationwith:resourceType = FS_ENGAGEMENTtoken.product = FINANCIAL_STATEMENTS- 30-day expiry
- SHA-256 token hash persisted; the raw JWT is never stored
- Enqueues
outboxeventfs.client.review_requested. - Emits
AuditEventfs.client.review_requested.
1.2 Outbound email
Subject: Financial statements ready for review: {Legal Name} {Financial Year}. Body: short note + single Open the review portal button linking to http://localhost:3000/portal/<jwt>.
1.3 Client clicks the link
The portal route verifies the JWT, looks up the matching PortalInvitation, and dispatches to the FS branch on token.product. The page renders:
- Engagement header — company name, financial year, year-end date, reporting standard, UEN
- A statements preview — BS / P&L / SOCE / Cash Flow summaries
- A DOCX download button
- Two CTAs: Approve and Request revision
1.4 Client decides
Approve path:
POST /portal/financial-statements/<token>/approveCaptures the decision; emits AuditEvent fs.client.approved; sends a confirmation email to the firm reviewer. Engagement remains at NOTES_COMPLETE.
Request revision path:
POST /portal/financial-statements/<token>/request-revision
Content-Type: application/json
{ "comment": "Please revise note 5 — the trade-receivables aging buckets should be 0-30 / 31-60 / 61-90 / 90+" }Captures the comment; emits AuditEvent fs.client.revision_requested; notifies the firm. The preparer addresses upstream (mapping / disclosure / notes) and re-runs the relevant step.
2. Verification
Database
SELECT id, token_hash IS NOT NULL AS has_hash, expires_at
FROM portal_invitations
WHERE resource_type = 'FS_ENGAGEMENT' AND resource_id = '<engagementId>'
ORDER BY created_at DESC LIMIT 1;Mailpit (http://localhost:8025)
One email to the configured approver contact, subject as above.
Audit log
| Event type | Notes |
|---|---|
fs.client.review_requested | Dispatch |
fs.client.approved | On approve |
fs.client.revision_requested | On revision request; payload contains the comment |
3. Negative & edge cases
- Magic link expired —
/portal/<token>returns401 Unauthorized. The reviewer reissues from the engagement; a fresh link is minted and the old one is invalidated. - No approver contact — handler refuses with
400 Bad Requestandreason: "no_approver_contact". The admin must add one first. - Multiple consecutive dispatches — each creates a fresh invitation; previous invitations remain valid until expiry. The activity log preserves both.
- Client tries to download a stale DOCX after a regenerate — the portal always serves the latest export. Versions are preserved server-side; the client only ever sees the latest.
Next
Proceed to FS-09 · Archive engagement.