FS-07 · Export publication-ready DOCX
SOP:
SOP_Financial_Statements.md§6 / Step 7.0Actors: FS Reviewer (fs-reviewer@spade.local), Senior Accountant, or Platform Admin —EXPORT_FS_DOCUMENTpermission. Pre-state: Engagement atNOTES_COMPLETE. LatestFsGeneratedStatement.isBalanced = true. Every mandatory disclosure rule answered. Post-state: A.docxartifact persisted to S3; signed URL returned. Engagement remains atNOTES_COMPLETE(export does not advance status).
0. Prerequisites
- Engagement at
NOTES_COMPLETE(see FS-06). - The caller holds
EXPORT_FS_DOCUMENT.FS_PREPARERdoes not hold this action — by design (SoD).
1. Steps
1.1 Trigger export
http
POST /ops/financial-statements/engagements/<engagementId>/exports/word
Authorization: Bearer <fs-reviewer-jwt>(Or click Export Word on the engagement detail page.)
The handler:
- Asserts
EXPORT_FS_DOCUMENT. - Asserts engagement is at
NOTES_COMPLETE. - Enqueues
export-statements-docxwith{engagementId, generatedStatementVersion?, requestedBy}. - Returns
202 Acceptedwith a polling URL.
1.2 Worker — export-statements-docx
- Reads the latest
FsGeneratedStatementfor the engagement (or the specifiedversionNo). - Reads every
FsNote. - Asserts:
isBalanced = true(or, with reviewer-override flag, emits a banner warning on the cover)- Every mandatory disclosure rule has every required prompt answered
- Every mandatory policy text in the notes matches the constitution verbatim (assertion against
mandatory_policies/*.md)
- Renders DOCX using
@breezycorp/financial-statements/output/word:- Cover page (company name, title, year-end date, UEN)
- Running header (company left, year-end right, thin bottom border)
- Centred 8 pt page numbers
- Directors Report (with financial-results table injected at placeholder position)
- SOFP (4-col: Description / Note / CY / PY)
- SPLOCI (2-col)
- SOCE (zero-movement columns auto-omitted)
- SOCF (Operating → Investing → Financing)
- Notes auto-numbered (1. General Information, 2. Basis, …)
- Persists to S3 at
financial-statements/<clientId>/<engagementId>/<filename>-v<n>.docx. - Inserts a
Filerow withfileKind = FS_DOCX_EXPORT. - Returns the signed URL.
- Emits
AuditEventfs.export.generatedwith{filename, fileId, versionNo}.
2. Verification
S3
bash
docker exec breezycorp-monorepo-minio-1 mc ls local/breezycorp/financial-statements/<clientId>/<engagementId>/
# expect a .docx file with a timestamp suffixDatabase
sql
SELECT id, original_name, storage_key, size_bytes
FROM files
WHERE client_id = '<clientId>' AND file_kind = 'FS_DOCX_EXPORT'
ORDER BY created_at DESC LIMIT 5;Audit log
| Event type | Notes |
|---|---|
fs.export.requested | API hit |
fs.export.generated | Background success; payload includes filename + signed URL + version |
Manual inspection
Download the DOCX. Open in Word. Confirm:
- Cover page shows the right company name, year-end, UEN
- Running header shows the company name on the left and the year-end on the right
- The Financial Results table appears inside the Directors Report at the right position
- Notes are auto-numbered starting from 1
- The income-tax policy paragraph in the policies note matches
constitution/mandatory_policies/income_tax.mdverbatim
3. Negative & edge cases
- Caller without
EXPORT_FS_DOCUMENT—403 Forbidden.FS_PREPARERis rejected. - Engagement not balanced — handler refuses with
422 Unprocessable Entityandreason: "engagement_not_balanced". Regenerate statements after fixing the mapping. - Mandatory disclosure missing — handler refuses with
reason: "mandatory_disclosure_missing". Open Interrogation. - Mandatory policy text mismatch — handler refuses with
reason: "mandatory_policy_mismatch". Regenerate notes (the verbatim assertion is on the constitution side; a paraphrased Claude response can land in the notes but won't pass the export gate). - Re-export — allowed; each export produces a new
.docxwith a newversionNo. All versions are retained for audit. - Export from
NOTES_ERROR— refused; the engagement must be atNOTES_COMPLETE.
Next
Proceed to FS-08 · Client portal sign-off (optional).