Skip to content

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 at NOTES_COMPLETE with at least one DOCX export available. Post-state: AuditEvent captures the client decision. Engagement remains at NOTES_COMPLETE. On revision request, the preparer addresses upstream and re-sends.

0. Prerequisites

  • Engagement at NOTES_COMPLETE with at least one DOCX export (see FS-07).
  • Client has at least one active contact with canApprove = true.

1. Steps

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

  1. Asserts MANAGE_FS_ENGAGEMENT.
  2. Creates a PortalInvitation with:
    • resourceType = FS_ENGAGEMENT
    • token.product = FINANCIAL_STATEMENTS
    • 30-day expiry
    • SHA-256 token hash persisted; the raw JWT is never stored
  3. Enqueues outbox event fs.client.review_requested.
  4. Emits AuditEvent fs.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>.

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:

http
POST /portal/financial-statements/<token>/approve

Captures the decision; emits AuditEvent fs.client.approved; sends a confirmation email to the firm reviewer. Engagement remains at NOTES_COMPLETE.

Request revision path:

http
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

sql
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 typeNotes
fs.client.review_requestedDispatch
fs.client.approvedOn approve
fs.client.revision_requestedOn revision request; payload contains the comment

3. Negative & edge cases

  • Magic link expired/portal/<token> returns 401 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 Request and reason: "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.

Internal use only — BreezyCorp