XBRL Filing & Financial Statements — Integration Plan
Status: Phase 0 complete · Phase 1 + Phase 2 foundations landed (constitution, schema, status machines, RBAC, export gate). Domain services, worker handlers, API routes, web pages, and AI plumbing remain — see §7 phased plan. Author: Architecture assessment (migration spec for xbrl-saas + finstatement source repos) Date: 2026-05-09 Related:
- bookkeeping-integration.md — closest precedent; same playbook, different product
- Source repos:
../../../../xbrl-saas/(XBRL SaaS — ACRA filing) and../../../../finstatement/(Financial statement generation) - Status machines now live: FS engagement · XBRL filing
What has landed in code (as of 2026-05-09)
The phased plan in §7 below describes the full migration. The list below is what exists in the monorepo today — anything not listed is still to come.
- Schema:
Client.fsConfig,Client.xbrlConfig. All 11 new aggregates:FsEngagement,FsTrialBalanceItem,FsGeneratedStatement,FsNote,FsDisclosureResponse,XbrlFiling,XbrlSourceDocument,XbrlLineItem,XbrlTagMapping,XbrlValidationResult,XbrlOutputArtifact. Migration20260509120000_add_fs_xbrl_aggregates(withrollback.sql). - Contracts:
Productextended withFINANCIAL_STATEMENTS,XBRL_FILING.Roleextended with the four new staff roles. 10 new TypeBox enums for FS + XBRL state types. NewPortalResourceTypeenum. - RBAC: All 20 new actions wired into
@breezycorp/auth/src/rbac.tswith the(role, product, action)matrix. The web app's mirror inapps/web/src/lib/dashboard-nav.tsis in sync (sidebar shows the new sections to the right roles). @breezycorp/financial-statements: constitution files (sfrs_taxonomy, disclosure_rules, mandatory_policies/), versioned loaders, disclosure trigger DSL evaluator (evaluateDisclosureGaps), 7-state engagement status machine.@breezycorp/xbrl: constitution files (taxonomy_acra_2026_v1, validation_rules_acra_2026_v1, mapping_library), versionedloadTaxonomy(version)(Rule 4),TaxonomyLookup(Rule 1),loadValidationRules+rulesForEntryPoint, mapping library withexactMatch+appendSynonym, 8-state filing status machine,assertExportAllowedenforcing Rules 2 & 3.- Tests: 42 new tests including the automated Rule 1 enforcer (greps
packages/xbrl/src/for ACRA element literals) and gate tests for Rules 2 & 3. - Build status:
pnpm build✓ (24/24 packages),pnpm test✓ (530+ tests),pnpm lint✓.
1. Context
BreezyCorp currently ships two products on one codebase:
- Payroll — monthly cycle automation around Infotech
- Bookkeeping — document → journal → upload-file flow + bank reconciliation around Xero / QuickBooks / Zoho / Tally
Two sibling Python codebases now need to be folded in as two additional first-class product modules:
- Financial Statements (
finstatementrepo) — annual / interim engagement: trial balance → SFRS-aligned mapping → fully computed Balance Sheet, P&L, SOCE, Cash Flow → AI-generated notes & Directors Report → publication-ready DOCX (and later PDF / XLSX). Singapore SFRS for Small Entities and Full SFRS. - XBRL Filing (
xbrl-saasrepo) — annual ACRA filing engagement: financial statement document (DOCX/PDF) or extracted line items → ACRA taxonomy 2026 v1.0 mapping → human-confirmed tag mappings → 144-rule validation → BizFinx Excel / XBRL XML output, gated by validation.
The two are tightly related: a typical Singapore client running both will produce financial statements first, then file XBRL from them. They are nonetheless distinct products with distinct domain models, distinct status machines, distinct UIs and distinct skill sets (an SFRS-fluent accountant prepares statements; a regulatory-trained preparer files XBRL).
Some clients will consume one or both; a few may consume neither (payroll-only or bookkeeping-only). A client using all four products should still see one login, one client master, one portal session.
2. Decision: Same Repo, Two New Product Modules
Build both as first-class product modules inside breezycorp-monorepo — do not keep the source Python repos alive and do not collapse them into one product.
Rationale
- Same customer, same portal session. A bookkeeping client whose monthly journals are already in our system is the same legal entity that, at year-end, needs financial statements and an XBRL filing. The
Clientmaster,ClientContactlist, file storage, audit trail, and magic-link portal must be shared. - Reuse is real. Magic-link auth, S3 file plumbing, OCR adapter, notifications, audit/outbox, ingestion channels (Drive/SharePoint), staff auth, observability, and the
Client.enabledProductsdiscriminator are already in place from the bookkeeping integration. The Phase 0 product-scoping refactor is done. - One operations team. Splitting these into two more repos = two more CI pipelines, two more deployment targets, two more migration trains, and a guaranteed cross-repo schema-drift problem.
- Cross-product flow. Bookkeeping's APPROVED journal entries → trial balance for Financial Statements → XBRL filing. This is the headline value proposition and only works cleanly if the data lives in one schema.
- Tech alignment. The source repos are Python/FastAPI + Supabase. The target stack is TypeScript/Fastify/Prisma. A direct port lets us delete the Python infrastructure, retire two Supabase projects, and consolidate auth, billing, and operations.
What this does NOT mean
- Financial Statements is not stuffed into Bookkeeping tables. Bookkeeping aggregates (
JournalBatch,JournalEntry,BankStatement,ReconciliationRun) stay bookkeeping. Financial Statements gets its own aggregates (FsEngagement,FsTrialBalance,FsLineItemMapping,FsGeneratedStatement,FsDisclosureResponse,FsNote). - XBRL Filing is not stuffed into Financial Statements tables. XBRL gets its own aggregates (
XbrlFiling,XbrlSourceDocument,XbrlLineItem,XbrlTagMapping,XbrlValidationResult,XbrlOutputArtifact). XBRL can ingest from aFsGeneratedStatementor from an independently uploaded DOCX/PDF — it does not require the Financial Statements module to be enabled. - Constitution files (taxonomy, validation rules, mapping library) live in the repo. They are immutable per release tag and are read by a loader, never string-literalled.
- Shared infrastructure (auth, files, OCR, notifications) is the integration layer, not the domain layer.
3. Reuse vs. Build — Inventory
| Layer | Reusable as-is | Net-new for FS | Net-new for XBRL |
|---|---|---|---|
| Auth / magic link | JWT tokens, session, staff MFA, portal token routing | Add FS_ENGAGEMENT to PortalResourceType (client review of generated statements) | Add XBRL_FILING to PortalResourceType (client sign-off before submission) |
| Client master | Client, ClientContact, enabledProducts array | fsConfig Json? (default reporting standard, base currency, year-end month, rounding) | xbrlConfig Json? (UEN, entity type, default entry point, taxonomy version pin) |
| RBAC | (role, product, action) matrix; hasProductPermission() | Add FS_PREPARER, FS_REVIEWER roles + FS-scoped actions | Add XBRL_PREPARER, XBRL_REVIEWER roles + XBRL-scoped actions |
| File storage | S3 client, File model, retention, key-prefix scoping | Prefix financial-statements/ for uploads (TB CSVs/XLSX) and outputs (DOCX/PDF) | Prefix xbrl/ for source docs (DOCX/PDF), constitution files, and outputs (XLSX/XML) |
| OCR / parsing | OcrAdapter interface (Vision + Claude / Mock), DocumentClassification, ExtractedField | New parser: trial balance CSV/XLSX (smart column detection, sign normalization) | New parsers: financial statement DOCX (python-docx-equivalent in TS — docx npm) and PDF (pdfplumber-equivalent — pdfjs-dist + pdf-parse) for line item extraction |
| AI / LLM adapter | None today; documents package uses Claude for OCR field extraction | New domain capability: Claude Sonnet for taxonomy mapping (TB account → SFRS key) and notes/Directors Report generation (consolidated 16K-token batch) | None initially; mapping is fuzzy-match-first via rapidfuzz-equivalent (TS: fuse.js or string-similarity). LLM fallback is a Phase 2 candidate |
| Notifications | Adapter, SMTP/mock, template renderer | New templates: TB uploaded, statements ready for review, notes generated, DOCX export ready | New templates: filing ready for review, validation passed/failed, BizFinx export ready |
| Audit / outbox | AuditEvent, OutboxEvent | New event types (FS_TB_UPLOADED, FS_MAPPING_CONFIRMED, FS_STATEMENT_GENERATED, FS_NOTES_GENERATED, FS_EXPORT_GENERATED) | New event types (XBRL_FILING_CREATED, XBRL_DOC_INGESTED, XBRL_MAPPING_CONFIRMED, XBRL_VALIDATION_RUN, XBRL_EXPORT_GENERATED) |
| Portal shell | /portal/[token] token verification, save-and-resume pattern | New per-engagement client review screen (statement preview + approve/request-revision) | New per-filing client sign-off screen (statement summary + UEN confirm + filing approve) |
| Exports | packages/ledger-exports — formatter interface, file-extension/mime-type metadata | New packages/financial-statements-output (DOCX via docx npm; later PDF via puppeteer/weasyprint-equivalent; XLSX via existing excel package) | New packages/xbrl-output (BizFinx multi-sheet XLSX; XBRL XML instance generator with arelle/equivalent validation hook) |
| Domain (status machines) | Status-machine pattern (canTransition, getNextStatuses) | New 7-state FS engagement machine | New 6-state XBRL filing machine + per-tag-mapping confirmation state |
| Constitution / taxonomy files | None today | packages/financial-statements/constitution/: sfrs_taxonomy.json (line items + sign + disclosure triggers), disclosure_rules.json (~25 rules, 50+ prompts), mandatory accounting policies | packages/xbrl/constitution/: taxonomy_acra_2026_v1.json (1871 elements), validation_rules_acra_2026_v1.json (144 rules), mapping_library.json (label → element_id synonyms; user-extendable) |
| API routes | Middleware, auth, route grouping (/portal, /ops/<product>, /admin, /hooks) | /ops/financial-statements/*; /portal/[token] extension for FS review | /ops/xbrl/*; /portal/[token] extension for XBRL sign-off |
| Worker handlers | Factory pattern, S3, observability, pg-boss | New: parse-trial-balance, auto-suggest-mapping, generate-statements, evaluate-disclosures, generate-notes, export-statements-docx | New: ingest-fs-document, extract-line-items, suggest-tag-mappings, run-xbrl-validation, generate-bizfinx-xlsx, generate-xbrl-xml |
| Web | Layout, auth scaffold, dashboard shell, portal shell | New /dashboard/financial-statements/* (engagements, TB upload, mapping, statements, interrogation, notes, exports) | New /dashboard/xbrl/* (filings, document upload, mapping, validation, exports) |
| Multi-tenancy | tenant_id is Client.id indirectly (no separate tenant table; staff are scoped via roles, clients via clientId foreign keys); RLS not used (Fastify enforces in plugins) | Same model; no schema additions | Source repo had tenants/tenant_members. Drop entirely — collapse into the existing Client + StaffUser + RBAC model |
4. Source Repositories — Feature Inventory
The following two sections are the canonical reference for what needs to ship. Anything not listed here and not in the source repos is out-of-scope for the migration. Anything listed here that does not land in BreezyCorp is a regression.
4.1 finstatement — Financial Statements Generation
Tech (source): FastAPI + Pydantic; React 19 + Vite + Tailwind + shadcn/ui; Supabase PostgreSQL; Anthropic Claude Sonnet 4 (claude-sonnet-4-20250514); python-docx, weasyprint, pandas/openpyxl. Deployed on Railway.
Purpose: Automate Singapore-compliant financial statement preparation for small entities (and Full SFRS where needed). Trial balance → SFRS taxonomy mapping → fully computed financial statements → AI-generated notes & Directors Report → publication-ready Word document.
Domain entities (Supabase tables):
| Table | Purpose | Key fields |
|---|---|---|
clients | Legal entity master | id, company_name, uen, registered_address, principal_activities, timestamps |
engagements | One financial-statement preparation project per client per year | id, client_id, financial_year (e.g. "FY2024"), year_end_date, reporting_standard (SFRS for Small Entities | Full SFRS), functional_currency (default SGD), rounding_convention (default SGD), status (7-state), has_prior_year, timestamps |
tb_line_items | One row per trial-balance GL account, append-replace per upload | id, engagement_id, account_code, account_description, current_year_balance (Decimal, debit-positive), prior_year_balance, taxonomy_key (SFRS key, e.g. cash, ppe, revenue), taxonomy_label, is_override (boolean — user manually changed AI suggestion) |
generated_statements | Latest fully computed statements as JSONB | id, engagement_id, statements_json (nested: balance_sheet, profit_and_loss, cash_flow_statement, statement_of_changes_in_equity, anomalies, is_balanced), generated_at |
notes | One row per note, content as structured JSON blocks | id, engagement_id, note_key (e.g. note_general, directors_report, note_ppe), title, content (array of {type: "paragraph"|"subheading"|"table", ...}), generated_at |
disclosure_responses | User answers to interrogation prompts | id, engagement_id, rule_id (e.g. DISC-001), prompt_id (e.g. DISC-001-04), response_data (JSONB {value: any}), is_complete |
Engagement status machine (7 states, source engagements.status):
draft → tb_uploaded → tb_mapped → statements_generated → notes_complete
↘ notes_error (recoverable)Features and use cases (source repo, fully implemented unless noted):
- Client CRUD —
POST/GET/PATCH/DELETE /api/v1/clients/. Frontend pages:Clients.jsx,NewClient.jsx. - Engagement CRUD —
POST/GET/PATCH/DELETE /api/v1/engagements/?client_id=.... Frontend:Engagements.jsx,NewEngagement.jsx. - Trial Balance Upload —
POST /api/v1/upload/{engagement_id}. CSV / XLSX / XLS, max 10 MB. Smart column detection (case-insensitive, underscore-agnostic): account code, description, debit/credit OR current-year/prior-year columns. Sign normalization (debit-positive). Year detection (4-digit headers, finds closest prior year). Replaces all prior TB items for engagement. Updates engagementstatus → tb_uploaded,has_prior_year. Frontend:Upload.jsxwith drag-drop and live preview. - Account Mapping (manual + AI auto-suggest) —
GET /api/v1/mapping/{engagement_id}— TB items with current taxonomy assignments.POST /api/v1/mapping/{engagement_id}/auto-suggest— sends TB + full SFRS taxonomy to Claude Sonnet; receives[{account_code, taxonomy_key, taxonomy_label, confidence: high|medium|low, rationale}]; upserts.POST /api/v1/mapping/{engagement_id}— manual override (setsis_override=true).POST /api/v1/mapping/{engagement_id}/validate— gap check.- Frontend:
Mapping.jsx— table with dropdowns, "Auto Suggest" button, confidence badges.
- Statements Generation —
POST /api/v1/statements/{engagement_id}/generate. Pure deterministic computation against the mapped TB:- Balance Sheet: Non-current assets → Current assets → Total assets ‖ Equity → Non-current liabilities → Current liabilities → Total equity & liabilities. Balancing check ±1.0 tolerance.
- P&L: Revenue (sign-flipped from credit) − COGS = GP; + Other Income − Selling/Admin/Finance/Other = PBT; − Tax = PAT.
- SOCE: opening equity + PAT + share movements = closing; auto-omit zero columns.
- Cash Flow (indirect,
cashflow_engine.py): Operating = PBT + D&A + working capital changes − tax paid; Investing = PPE/ROU/intangibles/investments; Financing = share/loan/lease movements. Reconciliation against opening + closing cash. - Anomaly detection: unbalanced, negative working capital, zero revenue, profit-but-no-tax.
- Persists to
generated_statements. Frontend:Statements.jsxtabbed view (BS / P&L / SOCE / CF).
- Disclosure Interrogation — Rule-engine driven by
disclosure_rules.json.GET /api/v1/interrogation/{engagement_id}/gaps— evaluates each rule'strigger_condition(alwaysortb_contains_any(['ppe', 'depreciation'])) against mapped TB keys; returns triggered, unanswered rules sorted by priority (mandatory → conditional → recommended).POST /api/v1/interrogation/{engagement_id}/respond— upsert{rule_id, responses: [{prompt_id, response_data}]}.- Stub endpoints:
/anomalies,/ratios,/variance. - Rule library (~25 rules, 50+ prompts). Mandatory: DISC-001 General Info, DISC-002 Reporting Standard, DISC-003 Going Concern, DISC-017 Income Tax, DISC-020 Directors Remuneration / Auditors / Audit Exemption, DISC-021 Employee Benefits, DISC-024 Events After Reporting Period, DISC-025 Dividends. Conditional (TB-triggered): DISC-010 PPE, DISC-011 Leases & ROU, DISC-012 Inventories, DISC-013 Trade Receivables, DISC-014 Cash, DISC-015 Share Capital, DISC-016 Borrowings, DISC-018 Revenue, DISC-022 Finance Costs. Recommended: DISC-019 Related Party Transactions, DISC-023 Commitments & Contingencies.
- Each prompt has
input_type:text | date | select | boolean | table | multi_text. Frontend:Interrogation.jsxaccordion form with conditional field visibility, progress bar, and regulatory callouts (SFRS sections, Companies Act refs).
- Notes & Directors Report Generation —
POST /api/v1/notes/{engagement_id}/auto-generate(background task; returns{status: "generating", message: "..."}immediately).- Background process: fetches disclosure responses, generated statements, engagement/client; groups responses by
note_key; builds one consolidated 16K-token Claude prompt with statements context + disclosure answers + verbatim mandatory accounting policies (Income Tax / Employee Benefits / Provisions) + Directors Report context. - One Claude call returns
{notes: [{note_key, title, blocks}], directors_report: {blocks}}. Block types:paragraph | subheading | table(withheadersandrows). A{type: "financial_results_placeholder"}block is replaced by Python-built financial-results table. - Upserts to
notes. Updates engagementstatus → notes_complete. GET /api/v1/notes/{engagement_id}/status— polling endpoint.GET /api/v1/notes/{engagement_id}— list all notes.- Mandatory generated notes:
directors_report(Section 201 Companies Act),note_general,note_basis,note_policies. Dynamic notes per TB content:note_ppe,note_trade_receivables,note_revenue,note_rpt, etc.
- Background process: fetches disclosure responses, generated statements, engagement/client; groups responses by
- Word Document Export —
POST /api/v1/output/{engagement_id}/word. Returns aStreamingResponsewith.docx. Document layout:- Cover page (company name, title, year-end date, UEN; blank header/footer)
- Running header (company left, year-end right, thin bottom border) and centered 8pt page numbers
- Directors Report (with injected Financial Results table at exact placeholder position)
- Statement of Financial Position (4-col: Description / Note / CY / PY)
- Statement of P&L and OCI (2-col)
- Statement of Changes in Equity (auto-omit zero columns)
- Statement of Cash Flows (Operating → Investing → Financing)
- Notes (each rendered from JSON blocks, paragraphs / subheadings / right-aligned amount tables; auto-numbered "1. General Information", "2. Basis", …)
- Stubs:
POST /api/v1/output/{engagement_id}/{pdf|excel|xbrl}.
- Auth — Stub only in source. Migrate to BreezyCorp staff auth + magic-link portal.
- Supporting schedules upload — Stub only. Out of scope for v1 migration; keep as backlog.
Configuration files (must be ported as constitution):
backend/app/data/sfrs_taxonomy.json— hierarchical taxonomy:balance_sheet(sections:non_current_assets,current_assets,equity,non_current_liabilities,current_liabilities) +profit_and_loss. Each item:key,label,section,sign(debit/credit),note_trigger(bool),disclosure_flags(e.g.,["depreciation_policy", "asset_class_movements_table"]),sfrs_se_section,full_sfrs_ref.backend/app/data/disclosure_rules.json— 25 rules with prompts, trigger conditions, priorities, andnote_keylinkage.
Test fixtures to port: test_tb.csv, test_tb_with_py.csv, financial_statements_final3.docx (sample output for visual regression).
Hardcoded Singapore assumptions to preserve (not parameterize in v1): SFRS for Small Entities, SFRS(I), Companies Act 1967 references, CPF, ACRA, SGD default, 12-month cycles.
4.2 xbrl-saas — XBRL Filing (ACRA)
Tech (source): FastAPI + Pydantic + Python 3.12; Next.js 16 + React 19 + TypeScript + Tailwind 4 + shadcn/ui; Supabase PostgreSQL with RLS + Supabase Auth (JWT); python-docx, pdfplumber (document ingest); rapidfuzz (label matching); openpyxl (BizFinx XLSX). Deployed on Railway.
Purpose: Singapore ACRA XBRL filing automation. Financial statement document → ACRA taxonomy 2026 v1.0 element mapping → human-confirmed tag mappings → 144-rule validation → BizFinx Excel / XBRL XML output, gated on validation pass and 100% human confirmation.
The five non-negotiable rules from cursorrules — these MUST survive the migration and must be re-asserted in code, tests, and code review:
- No taxonomy literal outside constitution files. All element references must flow through the loader (
packages/xbrl/src/taxonomy/loader.ts). Never string-literal an element ID inline. - No XBRL output without passing validation. The export endpoints check
XbrlValidationResultfor the filing. If any row hasseverity='error'andpassed=false, the export returns HTTP 422. - Every tag mapping must be human-confirmed. If any
XbrlTagMapping.confirmedById IS NULL, the export returns HTTP 422. Never auto-confirm. - Taxonomy version is first-class. The loader accepts a version parameter (e.g.,
acra_2026_v1) and loads the corresponding constitution file. Never assume the latest taxonomy applies to all filings — pin per filing. - Run regression suite before every merge. Test fixtures live in
packages/xbrl/__tests__/fixtures/.
Domain entities (Supabase tables in source — collapse tenants/tenant_members into BreezyCorp Client + StaffUser):
| Source table | Migration target | Notes |
|---|---|---|
tenants | DROP | Replaced by Client |
tenant_members | DROP | Replaced by StaffUser + (role, product, action) RBAC |
entities | Merge into Client (already has companyName, uen); add accountingStandard and fyEndMonth if not present | Source has UEN, accounting standard (SFRS / SFRS_SE / IFRS), FY-end month |
filings | XbrlFiling | New |
source_documents | XbrlSourceDocument (FK to existing File model) | Reuse File for S3 storage; XbrlSourceDocument is filing-scoped metadata |
line_items | XbrlLineItem | New |
tag_mappings | XbrlTagMapping | New |
validation_results | XbrlValidationResult | New |
output_artifacts | XbrlOutputArtifact (FK to File) | New |
audit_log | DROP | Replaced by existing AuditEvent |
Field-level model (target Prisma):
XbrlFiling—id,clientId,periodStart,periodEnd,entryPoint(FULL_XBRL|SIMPLIFIED_XBRL|FSH_BANKS|FSH_INSURANCE),taxonomyVersion(e.g.,acra_2026_v1),entityType(non_listed_company_full|smaller_non_listed_company|sgx_listed|mas_licensed_bank|mas_licensed_insurer),accountingStandard(SFRS|SFRS_SE|IFRS),status(6-state),fsEngagementId(nullable FK toFsEngagementif cross-product),currentVersion, timestamps.XbrlSourceDocument—id,xbrlFilingId,fileId(→File),sha256,mimeType,parseStatus.XbrlLineItem—id,xbrlFilingId,sourceDocumentId,statement(income_statement|balance_sheet|cash_flow|equity_changes|notes),label,valueCurrent(Decimal(20,2)),valuePrior(Decimal(20,2), nullable),sourcePage,sourceLine.XbrlTagMapping—id,xbrlFilingId,lineItemId(unique per filing),elementId(taxonomy element id),confidence(HIGH|MEDIUM|LOW|MANUAL),confirmedById(FKStaffUser, nullable),confirmedAt.XbrlValidationResult—id,xbrlFilingId,ruleId,severity(ERROR|WARNING|INFO),passed,message,relatedElements(string[]),runAt.XbrlOutputArtifact—id,xbrlFilingId,kind(BIZFINX_XLSX|XBRL_XML|AUDIT_PDF),fileId(→File),version,generatedAt.
Filing status machine (6 states):
DRAFT → DOCUMENTS_INGESTED → MAPPING_IN_PROGRESS → VALIDATION_PASSED → EXPORTED → FILED
↘ VALIDATION_FAILED (recoverable; back to MAPPING_IN_PROGRESS)Features and use cases (source endpoints; preserve all behaviors):
- Document upload —
POST /api/v1/filings/{filing_id}/documents. Multipart DOCX or PDF. Backend extracts tables (DOCX:python-docxparagraphs and tables; PDF:pdfplumbertables → text fallback), normalizes numbers, producesLineItem[](statement,label,value_current,value_prior,source_page,source_line). Auto-generatesMappingSuggestion[]via fuzzy match againstmapping_library.jsonexact synonyms first, then RapidFuzz ratio againsttaxonomy_acra_2026_v1.jsonelement labels. - Line items list —
GET /api/v1/filings/{filing_id}/line-items. Returns parsed line items with current confirmedelement_id(if any) and current top suggestion + alternatives. - Tag mapping confirmation —
POST /api/v1/filings/{filing_id}/mappings/{line_item_id}with{element_id, save_as_synonym?: bool}. SetsconfirmedByIdandconfirmedAt. Ifsave_as_synonymis true, appends{label, element_id}tomapping_library.json(writable constitution file — careful: requires audit + restricted to authorized roles). - Tag mapping unconfirm —
DELETE /api/v1/filings/{filing_id}/mappings/{line_item_id}. ClearsconfirmedByIdandconfirmedAt. - Synonym add —
POST /api/v1/mappings/save-synonymwith{label, element_id, scope?}. Direct mutation ofmapping_library.json. - Validation run —
POST /api/v1/filings/{filing_id}/validate. Loads all confirmed mappings; evaluates each of the 144 ACRA business rules (categories: arithmetical, totalling, cross-statement consistency, mandatory presence, sign conventions); writes results toXbrlValidationResult; returns{errors: [...], warnings: [...], info: [...]}. - BizFinx XLSX export —
POST /api/v1/filings/{filing_id}/export/bizfinx-excel. Validation-gated and confirmation-gated (rules 2 and 3 above). Multi-sheet XLSX: Income Statement, Balance Sheet, Cash Flow, Equity Changes, Filing Information. Each sheet rendered from confirmed mappings, routed by elementperiod_type(duration→ flow statements;instant→ balance sheet) andtaxonomy_section. Persists toXbrlOutputArtifact; returns download. - Health —
GET /health→{status: "ok"}. Map to existing/health/liveand/health/ready.
Constitution files (immutable per release; live in packages/xbrl/constitution/):
taxonomy_acra_2026_v1.json— 1871 elements. Each element:element_id(e.g.,sg-dei_NameOfCompany),label,data_type(stringItemType,monetaryItemType,decimalItemType, …),period_type(duration|instant|null),balance(debit|credit|null),minimum_requirement(bool — 43 elements),applicable_entry_points(string[]),taxonomy_section.validation_rules_acra_2026_v1.json— 144 rules. Each:rule_id(e.g.,BR_arithmetical_001),category,expression(DSL or JSON-encoded),severity,related_elements.mapping_library.json— fuzzy-match seed synonyms:{label_pattern, target_element_id, confidence}. Writable at runtime via the synonym endpoint (audit each write).- Source-of-truth Excel files (input to a build-time conversion script):
infrastructure/scripts/ACRA Taxonomy in Excel_2026_v1.0.xlsx,infrastructure/scripts/validationbusinessrules2026v1.xlsx, with conversion scriptstaxonomy_excel_to_json.pyandrules_excel_to_json.py. Port these scripts to TypeScript or keep them as build-time Python tools inpackages/xbrl/scripts/.
Test fixtures to port: apps/backend/tests/fixtures/ (sample DOCX/PDF, parsed line items, validation rule sets), test_export.xlsx (golden BizFinx output for snapshot testing).
Drop in migration (do not port): Supabase Auth code paths (TODOs in source); Celery scaffolding (not wired); per-tenant RLS (replaced by Fastify + clientId enforcement); audit_log table (use existing AuditEvent).
5. Target Architecture
5.1 Directory layout (post-migration)
apps/
api/
src/routes/
portal/
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW: client review of statements
xbrl/ ← NEW: client sign-off before filing
ops/
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW
engagements.ts
trial-balance.ts
mapping.ts
statements.ts
interrogation.ts
notes.ts
exports.ts
xbrl/ ← NEW
filings.ts
documents.ts
mappings.ts
validation.ts
exports.ts
synonyms.ts
admin/ ← extend with FS + XBRL config endpoints
hooks/ ← unchanged
worker/
src/handlers/
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW
parse-trial-balance.ts
auto-suggest-mapping.ts
generate-statements.ts
evaluate-disclosures.ts
generate-notes.ts
export-statements-docx.ts
xbrl/ ← NEW
ingest-fs-document.ts
extract-line-items.ts
suggest-tag-mappings.ts
run-xbrl-validation.ts
generate-bizfinx-xlsx.ts
generate-xbrl-xml.ts
shared/ ← existing (ocr-process, delete-s3-object, retention-purge, outbox-poller)
web/
src/app/
dashboard/
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW
engagements/ ← list, detail, new
[engagementId]/
upload/ ← TB drag-drop + preview
mapping/ ← TB → SFRS taxonomy table
statements/ ← BS/PL/SOCE/CF tabs + Generate + Export Word
interrogation/ ← disclosure accordion form
notes/ ← read-only generated notes preview
xbrl/ ← NEW
filings/ ← list, detail, new
[filingId]/
documents/ ← upload DOCX/PDF or pull from FS engagement
mappings/ ← line items + element confirmations + suggestions
validation/ ← errors/warnings/info; Validate button
exports/ ← BizFinx + XBRL XML downloads
portal/[token]/
(extended router: payroll | bookkeeping | financial-statements | xbrl)
packages/
contracts/
src/
enums/ ← Product (extended), Role (extended), Action (extended),
FsEngagementStatus, FsConfidenceLevel, FsReportingStandard,
XbrlFilingStatus, XbrlEntryPoint, XbrlEntityType,
XbrlAccountingStandard, XbrlConfidenceLevel,
XbrlValidationSeverity, XbrlOutputKind
schemas/
common/ ← existing
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW
xbrl/ ← NEW
domain/
src/
shared/ ← existing
payroll/ ← existing
bookkeeping/ ← existing
financial-statements/ ← NEW
services/
engagement.service.ts
trial-balance.service.ts
mapping.service.ts
statement-generator.service.ts
cashflow-engine.ts
disclosure-rule-engine.service.ts
notes-generator.service.ts
rules/
status-transitions.ts
taxonomy-loader.ts ← reads packages/financial-statements/constitution/sfrs_taxonomy.json
disclosure-loader.ts ← reads packages/financial-statements/constitution/disclosure_rules.json
xbrl/ ← NEW
services/
filing.service.ts
document-ingestion.service.ts
mapping-engine.service.ts
validation-engine.service.ts
export.service.ts
rules/
filing-status-transitions.ts
auth/ ← extended Role + Action
db/ ← one Prisma schema, new models added
documents/ ← extend OCR field schemas + add TB CSV/XLSX parser + DOCX/PDF line-item extractor
notifications/ ← add FS + XBRL templates
ledger-exports/ ← unchanged
excel/ ← stays Infotech-payroll-specific
financial-statements/ ← NEW
constitution/
sfrs_taxonomy.json
disclosure_rules.json
mandatory_policies/ ← verbatim policy text per topic
income_tax.md
employee_benefits.md
provisions.md
src/
taxonomy/loader.ts
disclosure/loader.ts
output/
word/ ← DOCX builder (npm `docx`)
pdf/ ← PDF builder (later, puppeteer/wkhtmltopdf)
excel/ ← (later, reuse @breezycorp/excel)
ai/
mapping-prompt.ts ← Claude Sonnet TB-to-taxonomy prompt
notes-prompt.ts ← Claude Sonnet 16K consolidated notes/Directors Report prompt
__tests__/
fixtures/
xbrl/ ← NEW
constitution/
taxonomy_acra_2026_v1.json
validation_rules_acra_2026_v1.json
mapping_library.json
scripts/
taxonomy_excel_to_json.{py|ts}
rules_excel_to_json.{py|ts}
src/
taxonomy/
loader.ts ← versioned: load(version: 'acra_2026_v1')
lookup.ts ← element queries (by id, by entry point, by section)
validation/
evaluator.ts ← rule expression evaluator
rules-loader.ts
mapping/
fuzzy-matcher.ts ← string-similarity / fuse.js
synonym-store.ts ← read/append mapping_library.json
ingestion/
docx-parser.ts ← npm `docx` reader / `mammoth` for tables
pdf-parser.ts ← `pdf-parse` + table extraction
normalizer.ts ← currency / decimal / sign normalization
output/
bizfinx-xlsx.ts ← multi-sheet workbook builder
xbrl-xml.ts ← XBRL instance document writer
__tests__/
fixtures/ ← golden DOCX/PDF, golden XLSX
observability/ ← unchanged5.2 Schema changes — cross-cutting
// Extend the existing Product enum (currently PAYROLL, BOOKKEEPING)
enum Product {
PAYROLL
BOOKKEEPING
FINANCIAL_STATEMENTS
XBRL_FILING
}
// Extend Role enum
enum Role {
CLIENT_SUBMITTER
CLIENT_APPROVER
PAYROLL_EXECUTIVE
PAYROLL_LEAD
BOOKKEEPER
SENIOR_ACCOUNTANT
PLATFORM_ADMIN
// NEW
FS_PREPARER // can manage TB mapping, generate statements/notes
FS_REVIEWER // FS_PREPARER rights + final approval + export
XBRL_PREPARER // can ingest documents, confirm mappings
XBRL_REVIEWER // XBRL_PREPARER rights + run validation + export + file
}
// Extend Action enum (product-scoped)
enum Action {
// ...existing payroll, bookkeeping, cross-product actions...
// Financial Statements
VIEW_FS_ENGAGEMENT
MANAGE_FS_ENGAGEMENT
UPLOAD_TRIAL_BALANCE
CONFIRM_FS_MAPPING
OVERRIDE_FS_MAPPING
GENERATE_STATEMENTS
RESPOND_DISCLOSURE
GENERATE_NOTES
EXPORT_FS_DOCUMENT
APPROVE_FS // client portal action
// XBRL
VIEW_XBRL_FILING
MANAGE_XBRL_FILING
INGEST_XBRL_DOCUMENT
CONFIRM_TAG_MAPPING
ADD_MAPPING_SYNONYM // restricted (writes to constitution-adjacent file)
RUN_XBRL_VALIDATION
EXPORT_BIZFINX
EXPORT_XBRL_XML
MARK_XBRL_FILED
APPROVE_XBRL // client portal action
}
model Client {
// existing fields including enabledProducts and bookkeepingConfig
fsConfig Json? // { defaultReportingStandard, baseCurrency, fyEndMonth, roundingConvention }
xbrlConfig Json? // { entityType, defaultEntryPoint, taxonomyVersion, accountingStandard }
}
// Extend PortalInvitation.resourceType (existing): add FS_ENGAGEMENT, XBRL_FILING
// --- Financial Statements aggregates ---
model FsEngagement {
id String @id @default(uuid())
clientId String @map("client_id")
client Client @relation(fields: [clientId], references: [id])
financialYear String @map("financial_year") // e.g. "FY2024"
yearEndDate DateTime @map("year_end_date") @db.Date
reportingStandard String @map("reporting_standard") // SFRS_SE | FULL_SFRS
functionalCurrency String @default("SGD") @map("functional_currency")
roundingConvention String @default("SGD") @map("rounding_convention")
status String // FsEngagementStatus enum
hasPriorYear Boolean @default(false) @map("has_prior_year")
versionNo Int @default(1) @map("version_no")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
trialBalanceItems FsTrialBalanceItem[]
generatedStatements FsGeneratedStatement[]
notes FsNote[]
disclosureResponses FsDisclosureResponse[]
@@index([clientId, status])
@@index([yearEndDate])
@@map("fs_engagements")
}
model FsTrialBalanceItem {
id String @id @default(uuid())
engagementId String @map("engagement_id")
engagement FsEngagement @relation(fields: [engagementId], references: [id], onDelete: Cascade)
accountCode String @map("account_code")
accountDescription String @map("account_description")
currentYearBalance Decimal @db.Decimal(20, 2) @map("current_year_balance")
priorYearBalance Decimal? @db.Decimal(20, 2) @map("prior_year_balance")
taxonomyKey String? @map("taxonomy_key")
taxonomyLabel String? @map("taxonomy_label")
mappingConfidence String? @map("mapping_confidence") // HIGH|MEDIUM|LOW
mappingRationale String? @map("mapping_rationale")
isOverride Boolean @default(false) @map("is_override")
uploadedAt DateTime @default(now()) @map("uploaded_at")
@@index([engagementId, accountCode])
@@map("fs_trial_balance_items")
}
model FsGeneratedStatement {
id String @id @default(uuid())
engagementId String @map("engagement_id")
engagement FsEngagement @relation(fields: [engagementId], references: [id], onDelete: Cascade)
versionNo Int @map("version_no")
statementsJson Json @map("statements_json")
isBalanced Boolean @map("is_balanced")
anomalies Json // array of {kind, message, fields}
generatedAt DateTime @default(now()) @map("generated_at")
@@index([engagementId, versionNo])
@@map("fs_generated_statements")
}
model FsNote {
id String @id @default(uuid())
engagementId String @map("engagement_id")
engagement FsEngagement @relation(fields: [engagementId], references: [id], onDelete: Cascade)
noteKey String @map("note_key")
title String
content Json // array of {type, text|headers|rows|...}
generatedAt DateTime @default(now()) @map("generated_at")
@@unique([engagementId, noteKey])
@@map("fs_notes")
}
model FsDisclosureResponse {
id String @id @default(uuid())
engagementId String @map("engagement_id")
engagement FsEngagement @relation(fields: [engagementId], references: [id], onDelete: Cascade)
ruleId String @map("rule_id") // e.g. DISC-001
promptId String @map("prompt_id") // e.g. DISC-001-04
responseData Json @map("response_data") // { value: ... }
isComplete Boolean @default(true) @map("is_complete")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@unique([engagementId, ruleId, promptId])
@@map("fs_disclosure_responses")
}
// --- XBRL Filing aggregates ---
model XbrlFiling {
id String @id @default(uuid())
clientId String @map("client_id")
client Client @relation(fields: [clientId], references: [id])
fsEngagementId String? @map("fs_engagement_id") // optional cross-product link
periodStart DateTime @map("period_start") @db.Date
periodEnd DateTime @map("period_end") @db.Date
entryPoint String @map("entry_point") // FULL_XBRL|SIMPLIFIED_XBRL|FSH_BANKS|FSH_INSURANCE
entityType String @map("entity_type") // non_listed_company_full|smaller_non_listed_company|sgx_listed|mas_licensed_bank|mas_licensed_insurer
accountingStandard String @map("accounting_standard") // SFRS|SFRS_SE|IFRS
taxonomyVersion String @map("taxonomy_version") // e.g. acra_2026_v1
status String // XbrlFilingStatus
versionNo Int @default(1) @map("version_no")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
sourceDocuments XbrlSourceDocument[]
lineItems XbrlLineItem[]
tagMappings XbrlTagMapping[]
validationResults XbrlValidationResult[]
outputArtifacts XbrlOutputArtifact[]
@@index([clientId, status])
@@index([periodEnd])
@@map("xbrl_filings")
}
model XbrlSourceDocument {
id String @id @default(uuid())
xbrlFilingId String @map("xbrl_filing_id")
filing XbrlFiling @relation(fields: [xbrlFilingId], references: [id], onDelete: Cascade)
fileId String @map("file_id")
file File @relation(fields: [fileId], references: [id])
sha256 String
mimeType String @map("mime_type")
parseStatus String @map("parse_status") // PENDING|PARSED|FAILED
pageCount Int? @map("page_count")
createdAt DateTime @default(now()) @map("created_at")
lineItems XbrlLineItem[]
@@index([xbrlFilingId])
@@map("xbrl_source_documents")
}
model XbrlLineItem {
id String @id @default(uuid())
xbrlFilingId String @map("xbrl_filing_id")
filing XbrlFiling @relation(fields: [xbrlFilingId], references: [id], onDelete: Cascade)
sourceDocumentId String @map("source_document_id")
sourceDocument XbrlSourceDocument @relation(fields: [sourceDocumentId], references: [id], onDelete: Cascade)
statement String // income_statement|balance_sheet|cash_flow|equity_changes|notes
label String
valueCurrent Decimal? @db.Decimal(20, 2) @map("value_current")
valuePrior Decimal? @db.Decimal(20, 2) @map("value_prior")
sourcePage Int @map("source_page")
sourceLine Int @map("source_line")
tagMapping XbrlTagMapping?
@@index([xbrlFilingId, statement])
@@map("xbrl_line_items")
}
model XbrlTagMapping {
id String @id @default(uuid())
xbrlFilingId String @map("xbrl_filing_id")
filing XbrlFiling @relation(fields: [xbrlFilingId], references: [id], onDelete: Cascade)
lineItemId String @unique @map("line_item_id")
lineItem XbrlLineItem @relation(fields: [lineItemId], references: [id], onDelete: Cascade)
elementId String @map("element_id") // e.g. sg-as_TotalAssets
confidence String // HIGH|MEDIUM|LOW|MANUAL
alternatives String[] // top-N suggestions stored for UI
explanation String?
confirmedById String? @map("confirmed_by_id")
confirmedBy StaffUser? @relation(fields: [confirmedById], references: [id])
confirmedAt DateTime? @map("confirmed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([xbrlFilingId])
@@index([elementId])
@@map("xbrl_tag_mappings")
}
model XbrlValidationResult {
id String @id @default(uuid())
xbrlFilingId String @map("xbrl_filing_id")
filing XbrlFiling @relation(fields: [xbrlFilingId], references: [id], onDelete: Cascade)
ruleId String @map("rule_id")
category String // arithmetical|totalling|cross_statement|mandatory|sign_convention|...
severity String // ERROR|WARNING|INFO
passed Boolean
message String
relatedElements String[] @map("related_elements")
runAt DateTime @default(now()) @map("run_at")
@@index([xbrlFilingId, severity, passed])
@@map("xbrl_validation_results")
}
model XbrlOutputArtifact {
id String @id @default(uuid())
xbrlFilingId String @map("xbrl_filing_id")
filing XbrlFiling @relation(fields: [xbrlFilingId], references: [id], onDelete: Cascade)
kind String // BIZFINX_XLSX|XBRL_XML|AUDIT_PDF
fileId String @map("file_id")
file File @relation(fields: [fileId], references: [id])
versionNo Int @map("version_no")
generatedAt DateTime @default(now()) @map("generated_at")
@@index([xbrlFilingId, kind])
@@map("xbrl_output_artifacts")
}Sign convention (must survive port for Financial Statements):
- TB storage: debit-positive, credit-negative.
- Presentation: all amounts shown as positive — flip liabilities/equity/income for presentation.
- Balancing tolerance: ±1.0 SGD.
Monetary precision (XBRL): Decimal(20,2) only. SGD with 2 decimal places.
5.3 RBAC matrix (additions)
| Role | FS Actions | XBRL Actions |
|---|---|---|
FS_PREPARER | VIEW_FS_ENGAGEMENT, MANAGE_FS_ENGAGEMENT, UPLOAD_TRIAL_BALANCE, CONFIRM_FS_MAPPING, OVERRIDE_FS_MAPPING, GENERATE_STATEMENTS, RESPOND_DISCLOSURE, GENERATE_NOTES | — |
FS_REVIEWER | All FS_PREPARER + EXPORT_FS_DOCUMENT | — |
XBRL_PREPARER | VIEW_FS_ENGAGEMENT (read-only, for cross-product reference) | VIEW_XBRL_FILING, MANAGE_XBRL_FILING, INGEST_XBRL_DOCUMENT, CONFIRM_TAG_MAPPING |
XBRL_REVIEWER | VIEW_FS_ENGAGEMENT | All XBRL_PREPARER + RUN_XBRL_VALIDATION, EXPORT_BIZFINX, EXPORT_XBRL_XML, MARK_XBRL_FILED, ADD_MAPPING_SYNONYM |
SENIOR_ACCOUNTANT | All FS actions | All XBRL actions except MARK_XBRL_FILED (regulator-facing — reserve for XBRL_REVIEWER and PLATFORM_ADMIN) |
PLATFORM_ADMIN | All | All |
CLIENT_APPROVER | APPROVE_FS (portal) | APPROVE_XBRL (portal) |
ADD_MAPPING_SYNONYM is intentionally restricted: it mutates a constitution-adjacent file (mapping_library.json) that affects all future filings for all clients. Each write must be audited (AuditEvent with action: ADD_MAPPING_SYNONYM, payload includes label, elementId, staffUserId).
5.4 Worker handlers
Every handler is idempotent, accepts a TypeBox-validated payload, and uses the existing pg-boss factory pattern with constructor-injected prisma, logger, storage, emailer.
Financial Statements:
parse-trial-balance— payload:{engagementId, fileId}. Reads CSV/XLSX from S3, smart column detection, sign normalization, year detection, append-replace intoFsTrialBalanceItem. Updates engagementstatus → TB_UPLOADED. EmitsFS_TB_UPLOADEDaudit event.auto-suggest-mapping— payload:{engagementId}. Calls Claude Sonnet with TB items + full SFRS taxonomy; upsertstaxonomyKey/taxonomyLabel/mappingConfidence/mappingRationaleper row. Updates engagementstatus → TB_MAPPEDif no rows remain unmapped.generate-statements— payload:{engagementId, requestedBy}. Pure deterministic computation (no AI). Builds BS / P&L / SOCE / CF (indirect method viacashflow-engine). Anomaly detection. Persists newFsGeneratedStatementrow (versioned). Updates engagementstatus → STATEMENTS_GENERATED.evaluate-disclosures— payload:{engagementId}. Loadsdisclosure_rules.json, evaluates each rule'strigger_conditionagainst mapped TB keys. Returns the gap list (no DB write — frontend reads gaps live).generate-notes— payload:{engagementId, requestedBy}. Background. Single Claude Sonnet 16K-token call combining statements + disclosure responses + verbatim mandatory policies. Parses JSON response, replacesfinancial_results_placeholderblock with computed table, upsertsFsNoterows. Updates engagementstatus → NOTES_COMPLETE(orNOTES_ERRORon failure with retry).export-statements-docx— payload:{engagementId, generatedStatementVersion?}. Builds DOCX using@breezycorp/financial-statements/output/word. Persists to S3 withfinancial-statements/<clientId>/<engagementId>/<filename>.docxkey. Returns signed URL.
XBRL:
ingest-fs-document— payload:{xbrlFilingId, fileId}. Reads DOCX/PDF from S3, computes sha256, persistsXbrlSourceDocument. Enqueuesextract-line-items.extract-line-items— payload:{sourceDocumentId}. Parses DOCX (viamammothordocx) or PDF (viapdf-parse+ table heuristics), normalizes amounts, persistsXbrlLineItemrows. Updates filingstatus → DOCUMENTS_INGESTED. Enqueuessuggest-tag-mappings.suggest-tag-mappings— payload:{xbrlFilingId}. For eachXbrlLineItemwithout a confirmed mapping: exact-match againstmapping_library.json; fall back to fuzzy-match (fuse.jsorstring-similarity) against taxonomy element labels filtered by entry point and statement type. PersistsXbrlTagMappingwithconfirmedById = NULL,alternativespopulated. Updates filingstatus → MAPPING_IN_PROGRESS.run-xbrl-validation— payload:{xbrlFilingId, requestedBy}. Loads all confirmed mappings + taxonomy + rules fortaxonomyVersion. Evaluates each rule's expression. Writes oneXbrlValidationResultper rule. Updates filingstatus → VALIDATION_PASSEDif no errors, elseVALIDATION_FAILED.generate-bizfinx-xlsx— payload:{xbrlFilingId, requestedBy}. Gate: check no error-severityXbrlValidationResultand noXbrlTagMappingwithconfirmedById IS NULL. On gate failure, write outbox event but do not produce artifact. On pass, build multi-sheet XLSX (Income Statement, Balance Sheet, Cash Flow, Equity Changes, Filing Information), persist to S3, writeXbrlOutputArtifact. Updates filingstatus → EXPORTED.generate-xbrl-xml— payload:{xbrlFilingId, requestedBy}. Same gate. Builds XBRL instance document with contexts (instant for balance sheet, duration for flow statements), units (SGD), and facts. Persists artifact.
5.5 API routes (additions)
All routes are protected by Fastify auth plugin with hasProductPermission(role, product, action) checks.
Financial Statements (apps/api/src/routes/ops/financial-statements/):
| Method | Path | Action | Purpose |
|---|---|---|---|
| POST | /ops/financial-statements/engagements | MANAGE_FS_ENGAGEMENT | Create engagement |
| GET | /ops/financial-statements/engagements | VIEW_FS_ENGAGEMENT | List (filter by clientId, status) |
| GET | /ops/financial-statements/engagements/:id | VIEW_FS_ENGAGEMENT | Detail |
| PATCH | /ops/financial-statements/engagements/:id | MANAGE_FS_ENGAGEMENT | Update (year-end, currency, standard) |
| DELETE | /ops/financial-statements/engagements/:id | MANAGE_FS_ENGAGEMENT | Soft delete (set status archive) |
| POST | /ops/financial-statements/engagements/:id/trial-balance | UPLOAD_TRIAL_BALANCE | Upload TB; enqueues parse-trial-balance |
| GET | /ops/financial-statements/engagements/:id/mapping | VIEW_FS_ENGAGEMENT | List TB items + mapping state |
| POST | /ops/financial-statements/engagements/:id/mapping/auto-suggest | CONFIRM_FS_MAPPING | Enqueue auto-suggest-mapping |
| PATCH | /ops/financial-statements/engagements/:id/mapping/:tbItemId | OVERRIDE_FS_MAPPING | Manual override (sets isOverride=true) |
| POST | /ops/financial-statements/engagements/:id/statements/generate | GENERATE_STATEMENTS | Enqueue generate-statements |
| GET | /ops/financial-statements/engagements/:id/statements | VIEW_FS_ENGAGEMENT | Latest FsGeneratedStatement |
| GET | /ops/financial-statements/engagements/:id/statements/:section | VIEW_FS_ENGAGEMENT | Single section: balance-sheet, income-statement, cash-flow, equity |
| GET | /ops/financial-statements/engagements/:id/disclosures/gaps | VIEW_FS_ENGAGEMENT | Live evaluation of disclosure_rules.json against mapped TB |
| POST | /ops/financial-statements/engagements/:id/disclosures/respond | RESPOND_DISCLOSURE | Upsert {ruleId, responses[]} |
| POST | /ops/financial-statements/engagements/:id/notes/auto-generate | GENERATE_NOTES | Enqueue generate-notes (background) |
| GET | /ops/financial-statements/engagements/:id/notes/status | VIEW_FS_ENGAGEMENT | Polling endpoint |
| GET | /ops/financial-statements/engagements/:id/notes | VIEW_FS_ENGAGEMENT | List notes |
| POST | /ops/financial-statements/engagements/:id/exports/word | EXPORT_FS_DOCUMENT | Enqueue + return signed URL |
Portal (Financial Statements client review):
| Method | Path | Purpose |
|---|---|---|
| GET | /portal/financial-statements/:token | Verify token; load engagement summary + statements preview |
| POST | /portal/financial-statements/:token/approve | Client approves; updates engagement, audit, notify staff |
| POST | /portal/financial-statements/:token/request-revision | Client requests revision with comment |
XBRL (apps/api/src/routes/ops/xbrl/):
| Method | Path | Action | Purpose |
|---|---|---|---|
| POST | /ops/xbrl/filings | MANAGE_XBRL_FILING | Create filing (clientId, period, entryPoint, entityType, taxonomyVersion) |
| GET | /ops/xbrl/filings | VIEW_XBRL_FILING | List |
| GET | /ops/xbrl/filings/:id | VIEW_XBRL_FILING | Detail |
| PATCH | /ops/xbrl/filings/:id | MANAGE_XBRL_FILING | Update header (rare) |
| POST | /ops/xbrl/filings/:id/documents | INGEST_XBRL_DOCUMENT | Multipart upload; enqueues ingest-fs-document |
| POST | /ops/xbrl/filings/:id/from-fs-engagement | INGEST_XBRL_DOCUMENT | Cross-product: produce line items from FsGeneratedStatement directly (skips DOCX parse) |
| GET | /ops/xbrl/filings/:id/line-items | VIEW_XBRL_FILING | List line items + mapping state + suggestions |
| POST | /ops/xbrl/filings/:id/mappings/:lineItemId | CONFIRM_TAG_MAPPING | Confirm {elementId, saveAsSynonym?: bool}; if saveAsSynonym requires ADD_MAPPING_SYNONYM |
| DELETE | /ops/xbrl/filings/:id/mappings/:lineItemId | CONFIRM_TAG_MAPPING | Unconfirm |
| POST | /ops/xbrl/mappings/synonyms | ADD_MAPPING_SYNONYM | Append {label, elementId} to mapping_library.json (audit) |
| POST | /ops/xbrl/filings/:id/validate | RUN_XBRL_VALIDATION | Enqueue run-xbrl-validation |
| GET | /ops/xbrl/filings/:id/validation-results | VIEW_XBRL_FILING | Latest results |
| POST | /ops/xbrl/filings/:id/exports/bizfinx | EXPORT_BIZFINX | Enqueue generate-bizfinx-xlsx (gated) |
| POST | /ops/xbrl/filings/:id/exports/xbrl-xml | EXPORT_XBRL_XML | Enqueue generate-xbrl-xml (gated) |
| GET | /ops/xbrl/filings/:id/exports | VIEW_XBRL_FILING | List artifacts with download URLs |
| POST | /ops/xbrl/filings/:id/mark-filed | MARK_XBRL_FILED | Records filing reference, status FILED |
Portal (XBRL client sign-off):
| Method | Path | Purpose |
|---|---|---|
| GET | /portal/xbrl/:token | Verify token; load filing summary + statement-level previews + validation pass status |
| POST | /portal/xbrl/:token/approve | Client signs off; updates filing |
| POST | /portal/xbrl/:token/request-revision | Client requests revision with comment |
Admin (extensions to apps/api/src/routes/admin/):
| Method | Path | Action | Purpose |
|---|---|---|---|
| PATCH | /admin/clients/:id/fs-config | MANAGE_CLIENTS | Update Client.fsConfig |
| PATCH | /admin/clients/:id/xbrl-config | MANAGE_CLIENTS | Update Client.xbrlConfig |
| GET | /admin/xbrl/taxonomy-versions | MANAGE_TEMPLATES | List installed taxonomy versions (read constitution dir) |
5.6 Web app — page inventory
Financial Statements (apps/web/src/app/dashboard/financial-statements/):
| Route | Page | Purpose |
|---|---|---|
/dashboard/financial-statements | EngagementsList | Table by client, status, year-end |
/dashboard/financial-statements/new | NewEngagement | Form: client, year-end, reporting standard, currency |
/dashboard/financial-statements/[engagementId] | EngagementOverview | Stage indicators (TB / Mapping / Statements / Disclosures / Notes / Export) |
/dashboard/financial-statements/[engagementId]/upload | UploadTB | Drag-drop, sample CSV link, parsed-result preview, "Replace upload" |
/dashboard/financial-statements/[engagementId]/mapping | Mapping | Table: account code, description, balance, taxonomy dropdown, confidence badge, AI rationale tooltip, "Auto Suggest" button |
/dashboard/financial-statements/[engagementId]/statements | Statements | Tabs: Balance Sheet, P&L, SOCE, Cash Flow. "Generate" button. Download buttons (Word now, PDF/XLSX later) |
/dashboard/financial-statements/[engagementId]/interrogation | Interrogation | Accordion of triggered rules, conditional fields, progress bar, regulatory callouts (SFRS section, Companies Act ref) |
/dashboard/financial-statements/[engagementId]/notes | Notes | Read-only preview of generated notes + Directors Report; "Regenerate" with confirm |
XBRL (apps/web/src/app/dashboard/xbrl/):
| Route | Page | Purpose |
|---|---|---|
/dashboard/xbrl | FilingsList | Table by client, period, entry point, status |
/dashboard/xbrl/new | NewFiling | Form: client, period, entry point, entity type, taxonomy version, optional fsEngagementId |
/dashboard/xbrl/[filingId] | FilingOverview | Stage indicators (Documents / Mappings / Validation / Exports) |
/dashboard/xbrl/[filingId]/documents | Documents | Upload DOCX/PDF or "Pull from Financial Statements engagement" |
/dashboard/xbrl/[filingId]/mappings | Mappings | Master-detail: line items list with confidence badges + element confirm/override panel; element search; "Save as synonym" for new exact matches |
/dashboard/xbrl/[filingId]/validation | Validation | Errors / Warnings / Info panes; "Validate" button; rule explanations |
/dashboard/xbrl/[filingId]/exports | Exports | Generate BizFinx, Generate XBRL XML, list versions, download |
Portal pages (apps/web/src/app/portal/[token]/):
The existing /portal/[token] router already dispatches by token.product. Add two new branches:
product === FINANCIAL_STATEMENTS→ render statement preview + Approve / Request Revisionproduct === XBRL_FILING→ render filing summary + Approve / Request Revision
5.7 Constitution files & loaders
Financial Statements (packages/financial-statements/constitution/):
sfrs_taxonomy.json— port verbatim fromfinstatement/backend/app/data/sfrs_taxonomy.json. Loader:loadSfrsTaxonomy(): SfrsTaxonomy.disclosure_rules.json— port verbatim fromfinstatement/backend/app/data/disclosure_rules.json. Loader:loadDisclosureRules(): DisclosureRule[].mandatory_policies/*.md— extract verbatim policy text (Income Tax, Employee Benefits, Provisions) currently embedded infinstatement/backend/app/api/notes.pyprompt. Expose viagetMandatoryPolicies(reportingStandard): { incomeTax, employeeBenefits, provisions }.
XBRL (packages/xbrl/constitution/):
taxonomy_acra_2026_v1.json— port verbatim. 1871 elements. Versioned filename to support multi-version loading.validation_rules_acra_2026_v1.json— port verbatim. 144 rules.mapping_library.json— port + writable. LoaderloadTaxonomy(version)MUST refuse to load unknown versions.
Conversion scripts (packages/xbrl/scripts/): Either port taxonomy_excel_to_json.py and rules_excel_to_json.py to TypeScript, or keep them as build-time Python tooling (run by maintainers when ACRA releases a new taxonomy). The output JSONs are the contract — the scripts themselves are not on any hot path.
5.8 AI / LLM integration
A new packages/ai-claude (or extend packages/documents) is needed for Financial Statements. It is not the OCR adapter — it's a higher-level domain capability for taxonomy mapping and notes generation.
Interface:
export interface ClaudeClient {
generateText(args: {
model: 'claude-sonnet-5' | 'claude-haiku-4-5';
systemPrompt: string;
userPrompt: string;
maxTokens: number;
temperature?: number;
}): Promise<{ text: string; inputTokens: number; outputTokens: number }>;
}Mock implementation (dev / tests): replays a fixture per prompt hash.
Real implementation: Anthropic SDK with prompt caching enabled (system prompt and the SFRS taxonomy block both cached). Use claude-sonnet-5 by default; claude-haiku-4-5 as a configurable step-down, claude-opus-5 for the hardest statements.
Two domain-level use cases initially:
- TB → SFRS taxonomy mapping (
packages/financial-statements/src/ai/mapping-prompt.ts):- Input: TB items + full taxonomy.
- Output:
[{accountCode, taxonomyKey, taxonomyLabel, confidence, rationale}]. - 4K output tokens. Schema-validated parse.
- Notes & Directors Report generation (
packages/financial-statements/src/ai/notes-prompt.ts):- Input: statements JSON + disclosure responses + mandatory policy texts + engagement/client metadata.
- Output:
{notes: [{noteKey, title, blocks}], directors_report: {blocks}}. - 16K output tokens. Single consolidated call (cost-optimized vs per-note loops).
- Block types:
paragraph | subheading | table | financial_results_placeholder(the placeholder is replaced post-LLM by deterministic financial results table).
XBRL does not use LLMs initially. Mapping is fuzzy-match against mapping_library.json exact synonyms first, then RapidFuzz-equivalent ratio against taxonomy element labels. LLM-assisted mapping suggestions are a Phase 2 candidate but must remain advisory — Rule 3 (every mapping human-confirmed) applies regardless.
5.9 Document I/O libraries
Financial Statements DOCX writing: docx npm package (active, TypeScript-first). Handles: cover page, headers/footers with borders, page numbers, tables with right-aligned amount columns, dynamic column omission for SOCE.
Financial Statements PDF (Phase 2): puppeteer rendering a Handlebars HTML template, OR call out to weasyprint from a sidecar Python container (matches source). Decide at Phase 2 kickoff.
Financial Statements XLSX (Phase 2): Reuse @breezycorp/excel package — already supports the BreezyCorp approval report so there is precedent.
XBRL DOCX/PDF reading: mammoth (DOCX → HTML/text + tables) and pdf-parse (PDF → text + table extraction). For complex PDF tables, fall back to pdfjs-dist + custom heuristics.
XBRL BizFinx XLSX writing: exceljs (active, supports multi-sheet, formulas, styling). Five sheets per filing.
XBRL XML writing: xmlbuilder2 (DOM-based). Contexts (instant for balance sheet, duration for income/cash-flow), units (SGD iso4217:SGD), and facts. Validation hook (Phase 2): subprocess call to arelle for true XBRL conformance check before marking filing FILED.
6. Cross-Product Data Flow
6.1 The headline flow: monthly bookkeeping → annual financial statements → annual XBRL filing
[Bookkeeping] [Financial Statements] [XBRL Filing]
JournalBatch (APPROVED) ─┐
JournalBatch (APPROVED) ├──► FsEngagement.trialBalance (auto-pull) ──► XbrlFiling.fromFsEngagement
JournalBatch (APPROVED) ─┘ (year-aggregated, by COA) (line items pre-mapped from
↓ FsGeneratedStatement)
Mapping → SFRS taxonomy keys
↓
Statements generated (BS / PL / SOCE / CF)
↓
Disclosure interrogation
↓
Notes + Directors Report (Claude)
↓
DOCX export (client review) ──► XBRL ingestion
(DOCX → line items, OR direct
from FsGeneratedStatement)
↓
Tag mapping confirmation
↓
144-rule validation
↓
BizFinx XLSX / XBRL XML
↓
Mark filed (ACRA)Cross-product affordances to build:
- Bookkeeping → Financial Statements: A button on the FS engagement upload screen — "Pull trial balance from bookkeeping (FY2024)". Aggregates all
JournalEntryrows inJournalBatchrows withstatus=EXPORTEDfor the period, byaccountCode. Skips manual TB upload. (Phase 2 — not v1.) - Financial Statements → XBRL Filing: The
POST /ops/xbrl/filings/:id/from-fs-engagementendpoint. Reads the latestFsGeneratedStatement.statementsJson, expands intoXbrlLineItemrows, and auto-confirms mappings where the SFRS taxonomy key has a known ACRA element correspondence inmapping_library.jsonwithconfidence=HIGH. (Phase 2.)
Phase 1 keeps the products independent: TB is uploaded directly, XBRL ingests its own DOCX/PDF. Cross-product wiring is a deliberate Phase 2 enhancement once both products are in production with a few cycles each.
6.2 Independent flows (Phase 1 baseline)
Financial Statements (independent):
1. Staff: Create FsEngagement (clientId, FY, year-end, standard, currency)
POST /ops/financial-statements/engagements
2. Staff: Upload trial balance CSV/XLSX
POST /ops/financial-statements/engagements/:id/trial-balance
→ enqueues parse-trial-balance → FsTrialBalanceItem rows; status TB_UPLOADED
3. Staff: Auto-suggest mapping (Claude)
POST /ops/financial-statements/engagements/:id/mapping/auto-suggest
→ enqueues auto-suggest-mapping; per-row taxonomyKey + confidence + rationale; status TB_MAPPED
4. Staff: Override low-confidence rows manually
PATCH /ops/financial-statements/engagements/:id/mapping/:tbItemId
5. Staff: Generate statements (deterministic)
POST /ops/financial-statements/engagements/:id/statements/generate
→ enqueues generate-statements; FsGeneratedStatement v1; status STATEMENTS_GENERATED
6. Staff: Open interrogation; fill triggered rules
GET /ops/financial-statements/engagements/:id/disclosures/gaps
POST /ops/financial-statements/engagements/:id/disclosures/respond (per rule)
7. Staff: Generate notes (Claude, single 16K-token call)
POST /ops/financial-statements/engagements/:id/notes/auto-generate
→ enqueues generate-notes; FsNote rows + directors_report; status NOTES_COMPLETE
8. Staff: Export Word
POST /ops/financial-statements/engagements/:id/exports/word → signed URL
9. (Optional) Send to client for review via portal token
token.product = FINANCIAL_STATEMENTS, token.resourceType = FS_ENGAGEMENT
Client opens /portal/[token] → preview → Approve / Request RevisionXBRL (independent):
1. Staff: Create XbrlFiling (clientId, period, entry point, entity type, taxonomy version)
POST /ops/xbrl/filings
2. Staff: Upload financial statement DOCX or PDF
POST /ops/xbrl/filings/:id/documents
→ enqueues ingest-fs-document → extract-line-items → suggest-tag-mappings; status MAPPING_IN_PROGRESS
3. Staff: Open mappings UI; confirm or override every line item's element ID
POST /ops/xbrl/filings/:id/mappings/:lineItemId (per row, with optional saveAsSynonym)
4. Staff: Run validation
POST /ops/xbrl/filings/:id/validate
→ enqueues run-xbrl-validation; XbrlValidationResult rows; status VALIDATION_PASSED or VALIDATION_FAILED
5. (If failed) Staff revisits mappings; repeat 3–4
6. Staff: Generate BizFinx Excel (gated on validation pass + all mappings confirmed)
POST /ops/xbrl/filings/:id/exports/bizfinx → signed URL; status EXPORTED
7. Staff: Generate XBRL XML (same gate)
POST /ops/xbrl/filings/:id/exports/xbrl-xml → signed URL
8. (Optional) Send to client for sign-off via portal token
token.product = XBRL_FILING, token.resourceType = XBRL_FILING
9. Staff: Mark filed (with ACRA filing reference)
POST /ops/xbrl/filings/:id/mark-filed; status FILED7. Phased Migration Plan
Each phase is a complete, deployable increment. No phase leaves the trunk in a broken state. After each phase, all existing payroll + bookkeeping flows must still pass their manual playbooks.
Phase 0 — Cross-cutting prep (1–2 days)
- [ ] Extend
Productenum:FINANCIAL_STATEMENTS,XBRL_FILING. - [ ] Extend
Roleenum:FS_PREPARER,FS_REVIEWER,XBRL_PREPARER,XBRL_REVIEWER. - [ ] Extend
Actionenum (see §5.2 list). - [ ] Extend
PortalResourceTypeenum:FS_ENGAGEMENT,XBRL_FILING. - [ ] Extend
Clientmodel: add nullablefsConfig Json?,xbrlConfig Json?. - [ ] Migration:
add_fs_xbrl_product_scaffolding. - [ ] Add
hasProductPermissionmatrix entries. - [ ] Tests: enum exhaustiveness, RBAC matrix coverage.
Phase 1 — Financial Statements MVP (2–3 weeks)
Goal: A staff user can take a Singapore SME from TB upload to a publication-ready DOCX, end-to-end, in our app.
- [ ] Constitution port. Copy
sfrs_taxonomy.json+disclosure_rules.jsonverbatim intopackages/financial-statements/constitution/. Extract mandatory policy texts fromfinstatementnotes.pyintomandatory_policies/*.md. - [ ] Schema. Add
FsEngagement,FsTrialBalanceItem,FsGeneratedStatement,FsNote,FsDisclosureResponse. Migrationadd_financial_statements_aggregates. - [ ] Domain layer.
EngagementService, status machinestatus-transitions.ts,loadSfrsTaxonomy,loadDisclosureRules,getMandatoryPolicies.TrialBalanceServicewith smart CSV/XLSX parser (porttb_parser.pylogic — column detection, sign normalization, year detection).StatementGenerator(portstatements.pyBS/PL/SOCE assembly +cashflow_engine.pyindirect method).DisclosureRuleEngine(port thetb_contains_any(...)evaluator and rule sorting).MappingService+ClaudeMappingPrompt(portmapping.py).NotesGeneratorService+ClaudeNotesPrompt(portnotes.py16K-token consolidated call).
- [ ] Worker handlers. All six FS handlers (§5.4).
- [ ] API routes. All FS endpoints (§5.5) including admin
fs-config. - [ ] AI plumbing.
packages/ai-claude(or extension ofdocuments). Mock implementation for tests. Anthropic SDK in production with prompt caching on system prompt + taxonomy block. - [ ] DOCX export.
packages/financial-statements/output/word. Port the layout fromoutput.pyprecisely (cover page, header/footer borders, page numbers, financial results table position, dynamic SOCE columns, note auto-numbering). Visual regression againstfinancial_statements_final3.docx. - [ ] Portal extension.
/portal/[token]route handlesproduct === FINANCIAL_STATEMENTS. New/portal/financial-statements/[token]page with preview + Approve / Request Revision. New email template "Statements ready for review". - [ ] Web app. All FS pages (§5.6) using shadcn/ui + the existing dashboard shell.
- [ ] Notifications. Templates:
fs.tb_uploaded,fs.statements_ready_for_review,fs.notes_generated,fs.docx_exported,fs.client_approved,fs.client_requested_revision. - [ ] Audit events. All FS event types.
- [ ] Seed data. Add a fourth demo client (
AURORA) — financial-statements-only, FY2024 with a small TB, in statusTB_MAPPED. - [ ] Tests.
- Domain: status transitions, deterministic statement generation against the
test_tb_with_py.csvfixture. - Disclosure rule evaluation (every rule's trigger condition).
- Notes prompt regression (frozen Claude response replay).
- Word export visual regression (snapshot of generated DOCX byte structure or rendered HTML).
- Domain: status transitions, deterministic statement generation against the
- [ ] Manual playbook.
docs/financial-statements/quick-start.mdwalking through Aurora end-to-end.
Phase 2 — XBRL Filing MVP (2–3 weeks)
Goal: A staff user can take a financial statement DOCX → confirmed mappings → validated → BizFinx XLSX, end-to-end. The five non-negotiable rules from cursorrules pass code review.
- [ ] Constitution port. Copy
taxonomy_acra_2026_v1.json(1871 elements),validation_rules_acra_2026_v1.json(144 rules),mapping_library.jsonintopackages/xbrl/constitution/. Port the Excel-to-JSON conversion scripts (or document the manual workflow). - [ ] Schema. Add
XbrlFiling,XbrlSourceDocument,XbrlLineItem,XbrlTagMapping,XbrlValidationResult,XbrlOutputArtifact. Migrationadd_xbrl_filing_aggregates. - [ ] Domain layer.
FilingService, status machine.TaxonomyLoader.load(version)— refuses unknown versions. Rule 1 enforced in code review.TaxonomyLookup— by id, by entry point, by section. No string-literal element IDs anywhere else.DocumentIngestion— DOCX (mammoth) and PDF (pdf-parse) parsers; line-item normalizer.MappingEngine— exact synonym match → fuzzy match against entry-point-filtered elements; suggestions persisted withconfirmedBy = NULL.ValidationEngine— rule expression evaluator; rule taxonomy-version-aware.ExportService.bizfinxXlsx,ExportService.xbrlXml— both checkvalidationResults(no error-severity, all passed) AND alltagMappings.confirmedBy != NULL. Rules 2 and 3 enforced as service-layer guards with explicitXbrlExportBlockedErrorand matching HTTP 422.SynonymStore.append({label, elementId, staffUserId})— writesmapping_library.json, emits audit event, restricted toADD_MAPPING_SYNONYMpermission.
- [ ] Worker handlers. All six XBRL handlers (§5.4). The
generate-bizfinx-xlsxandgenerate-xbrl-xmlhandlers re-check the gate (defense in depth). - [ ] API routes. All XBRL endpoints (§5.5) including admin
xbrl-configandtaxonomy-versions. - [ ] Output libraries.
exceljsfor BizFinx multi-sheet workbook (Income Statement, Balance Sheet, Cash Flow, Equity Changes, Filing Information).xmlbuilder2for XBRL XML instance with contexts/units/facts. - [ ] Portal extension.
/portal/[token]handlesproduct === XBRL_FILING. New/portal/xbrl/[token]page with statement summary + UEN confirm + Approve / Request Revision. - [ ] Web app. All XBRL pages (§5.6).
- [ ] Notifications. Templates:
xbrl.documents_ingested,xbrl.validation_passed,xbrl.validation_failed,xbrl.bizfinx_ready,xbrl.xbrl_xml_ready,xbrl.client_approved,xbrl.client_requested_revision,xbrl.marked_filed. - [ ] Audit events. All XBRL event types.
ADD_MAPPING_SYNONYMwrites are also audited. - [ ] Seed data. Add a fifth demo client (
STELLAR) — XBRL-only, with one filing inMAPPING_IN_PROGRESS(3 unmapped line items, 5 confirmed) and one inVALIDATION_FAILED(with 2 errors and 5 warnings). - [ ] Tests.
- Constitution-rule enforcement: a unit test that greps
packages/xbrl/src/**/*.tsforsg-as_/sg-dei_/sg-bp_literals outsidetaxonomy/and fails the build if any are found. Rule 1 enforced by test. - Export gate: tests that exports raise 422 on missing confirmation OR validation error. Rules 2 and 3 enforced by tests.
- Taxonomy version: a test that loading an unknown version raises a typed error. Rule 4 enforced by test.
- Validation engine: per-rule unit tests using fixtures from
apps/backend/tests/fixtures/. Golden BizFinx XLSX snapshot test againsttest_export.xlsx.
- Constitution-rule enforcement: a unit test that greps
- [ ] Manual playbook.
docs/xbrl/quick-start.mdwalking through Stellar end-to-end. - [ ] Code-review checklist added to
docs/reference/architecture/xbrl-finstatement-integration.md(this file): the five non-negotiable rules + how each is enforced.
Phase 3 — Cross-product wiring (1 week)
- [ ]
POST /ops/xbrl/filings/:id/from-fs-engagement— pull line items directly fromFsGeneratedStatement.statementsJson; auto-confirm mappings where the SFRS key → ACRA element mapping inmapping_library.jsonisHIGHconfidence; leaveMEDIUM/LOWunconfirmed for manual review. - [ ] Bookkeeping → FS: "Pull trial balance from bookkeeping" button on the FS upload screen. Aggregates
JournalEntry(status=APPROVED) byaccountCodefor the engagement period. - [ ] Notification: when an FS engagement is approved by client, a follow-up suggestion email goes to the XBRL team for that client (if
XBRL_FILINGis inenabledProducts). - [ ] Tests for the cross-product flows.
Phase 4 — Polishing & operational readiness (ongoing)
- [ ] FS PDF export.
- [ ] FS XLSX export.
- [ ] XBRL XML conformance check via
arellesidecar. - [ ] Per-client custom mapping libraries (override the global
mapping_library.jsonperclientId). - [ ] Disclosure interrogation save-and-resume in client portal (currently staff-only).
- [ ] XBRL "Mark filed" extension: capture ACRA filing reference + filed-by-staff + filed-on, immutable.
- [ ] Retention policy entries for FS + XBRL artifacts.
- [ ] Runbook entries.
- [ ] PII inventory updates (Directors' personal data appears in financial statements; UEN is regulated PII per PDPA).
8. Code-Review Checklist (XBRL — the five non-negotiables)
When reviewing any XBRL-related PR, the reviewer must confirm each item below. These are not aspirational — they are gating.
- No taxonomy literal outside constitution files.
- [ ] No occurrences of
sg-as_,sg-dei_,sg-bp_,sg-fs_, etc. inpackages/xbrl/src/outsidetaxonomy/loader.tsandtaxonomy/lookup.ts. - [ ] All element references go through
TaxonomyLookup.
- [ ] No occurrences of
- No XBRL output without passing validation.
- [ ]
ExportService.bizfinxXlsxandExportService.xbrlXmlcheckvalidationResultsfor anyseverity = ERRORandpassed = falseand reject with HTTP 422. - [ ] Worker handlers
generate-bizfinx-xlsxandgenerate-xbrl-xmlre-check the gate (defense in depth).
- [ ]
- Every tag mapping must be human-confirmed.
- [ ]
ExportServicerejects if anyXbrlTagMapping.confirmedById IS NULL. - [ ] No code path sets
confirmedByIdautomatically (only in response to thePOST .../mappings/:lineItemIdendpoint with an authenticated staff user).
- [ ]
- Taxonomy version is first-class.
- [ ]
TaxonomyLoader.load(version)is the only entry point; refuses unknown versions. - [ ]
XbrlFiling.taxonomyVersionis required on creation and immutable thereafter. - [ ] All validation rule loading respects the filing's pinned version.
- [ ]
- Run regression suite before push.
- [ ]
pnpm --filter @breezycorp/xbrl testpasses. - [ ] Snapshot test against
test_export.xlsxgolden output passes.
- [ ]
9. Open Questions / Risks
- DOCX-to-XBRL parsing fidelity. The
xbrl-saassource usespython-docx+pdfplumber; the TS equivalents (mammoth,pdf-parse) have different table-extraction characteristics. Plan: build with the test fixtures fromapps/backend/tests/fixtures/, run snapshot diffs early in Phase 2, and budget for a fallback to a Python sidecar parser if the TS-only path leaves edge cases. - Claude prompt-caching strategy. The Notes prompt is large (statements + disclosures + mandatory policies + 16K output). Confirm cost numbers before Phase 1 ships; consider caching the system prompt and the per-engagement statements block.
- Mandatory policy text governance. The verbatim policy text is regulated content (SFRS for Small Entities Section 29, etc.). Decide how updates are handled: PRs with sign-off from a qualified accountant; document as runbook.
- arelle integration timing. XBRL XML output must be schema-valid for ACRA acceptance. We can ship Phase 2 without arelle and rely on our 144 rules + manual review for the first few filings, then add arelle in Phase 4.
- Multi-tenant constitution writes.
mapping_library.jsonis a single file shared across all clients. A synonym added for one client affects future filings for all clients. This is intentional in the source repo but may not match a multi-tenant production assumption. Plan: confirm with the team whether per-client overrides (Phase 4) are needed before Phase 2 ships, or whether the global library with restricted-role writes is acceptable. - Source repo deletion timing. The Python repos remain the operational source of truth until Phases 1 and 2 are fully validated in production with parallel runs against real engagements. Plan a 2-cycle parallel-run window before shutdown.
10. References
- BreezyCorp project guide:
CLAUDE.md - Bookkeeping integration plan (precedent):
docs/reference/architecture/bookkeeping-integration.md - Source:
../../../../finstatement/(Python/FastAPI + React/Vite — financial statements) - Source:
../../../../xbrl-saas/(Python/FastAPI + Next.js — XBRL filing) - Source
cursorrules(XBRL — the five non-negotiable rules):../../../../xbrl-saas/cursorrules - ACRA taxonomy reference (input to constitution):
../../../../xbrl-saas/infrastructure/scripts/ACRA Taxonomy in Excel_2026_v1.0.xlsx - ACRA validation rules reference:
../../../../xbrl-saas/infrastructure/scripts/validationbusinessrules2026v1.xlsx - SFRS taxonomy (input to constitution):
../../../../finstatement/backend/app/data/sfrs_taxonomy.json - Disclosure rules (input to constitution):
../../../../finstatement/backend/app/data/disclosure_rules.json - Visual regression baseline:
../../../../finstatement/financial_statements_final3.docx - TB test fixtures:
../../../../finstatement/test_tb.csv,../../../../finstatement/test_tb_with_py.csv - BizFinx golden snapshot:
../../../../xbrl-saas/test_export.xlsx