Skip to content

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. Migration 20260509120000_add_fs_xbrl_aggregates (with rollback.sql).
  • Contracts: Product extended with FINANCIAL_STATEMENTS, XBRL_FILING. Role extended with the four new staff roles. 10 new TypeBox enums for FS + XBRL state types. New PortalResourceType enum.
  • RBAC: All 20 new actions wired into @breezycorp/auth/src/rbac.ts with the (role, product, action) matrix. The web app's mirror in apps/web/src/lib/dashboard-nav.ts is 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), versioned loadTaxonomy(version) (Rule 4), TaxonomyLookup (Rule 1), loadValidationRules + rulesForEntryPoint, mapping library with exactMatch + appendSynonym, 8-state filing status machine, assertExportAllowed enforcing 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 (finstatement repo) — 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-saas repo) — 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

  1. 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 Client master, ClientContact list, file storage, audit trail, and magic-link portal must be shared.
  2. Reuse is real. Magic-link auth, S3 file plumbing, OCR adapter, notifications, audit/outbox, ingestion channels (Drive/SharePoint), staff auth, observability, and the Client.enabledProducts discriminator are already in place from the bookkeeping integration. The Phase 0 product-scoping refactor is done.
  3. 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.
  4. 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.
  5. 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 a FsGeneratedStatement or 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

LayerReusable as-isNet-new for FSNet-new for XBRL
Auth / magic linkJWT tokens, session, staff MFA, portal token routingAdd FS_ENGAGEMENT to PortalResourceType (client review of generated statements)Add XBRL_FILING to PortalResourceType (client sign-off before submission)
Client masterClient, ClientContact, enabledProducts arrayfsConfig 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 actionsAdd XBRL_PREPARER, XBRL_REVIEWER roles + XBRL-scoped actions
File storageS3 client, File model, retention, key-prefix scopingPrefix 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 / parsingOcrAdapter interface (Vision + Claude / Mock), DocumentClassification, ExtractedFieldNew 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 adapterNone today; documents package uses Claude for OCR field extractionNew 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
NotificationsAdapter, SMTP/mock, template rendererNew templates: TB uploaded, statements ready for review, notes generated, DOCX export readyNew templates: filing ready for review, validation passed/failed, BizFinx export ready
Audit / outboxAuditEvent, OutboxEventNew 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 patternNew per-engagement client review screen (statement preview + approve/request-revision)New per-filing client sign-off screen (statement summary + UEN confirm + filing approve)
Exportspackages/ledger-exports — formatter interface, file-extension/mime-type metadataNew 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 machineNew 6-state XBRL filing machine + per-tag-mapping confirmation state
Constitution / taxonomy filesNone todaypackages/financial-statements/constitution/: sfrs_taxonomy.json (line items + sign + disclosure triggers), disclosure_rules.json (~25 rules, 50+ prompts), mandatory accounting policiespackages/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 routesMiddleware, 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 handlersFactory pattern, S3, observability, pg-bossNew: parse-trial-balance, auto-suggest-mapping, generate-statements, evaluate-disclosures, generate-notes, export-statements-docxNew: ingest-fs-document, extract-line-items, suggest-tag-mappings, run-xbrl-validation, generate-bizfinx-xlsx, generate-xbrl-xml
WebLayout, auth scaffold, dashboard shell, portal shellNew /dashboard/financial-statements/* (engagements, TB upload, mapping, statements, interrogation, notes, exports)New /dashboard/xbrl/* (filings, document upload, mapping, validation, exports)
Multi-tenancytenant_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 additionsSource 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):

TablePurposeKey fields
clientsLegal entity masterid, company_name, uen, registered_address, principal_activities, timestamps
engagementsOne financial-statement preparation project per client per yearid, 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_itemsOne row per trial-balance GL account, append-replace per uploadid, 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_statementsLatest fully computed statements as JSONBid, engagement_id, statements_json (nested: balance_sheet, profit_and_loss, cash_flow_statement, statement_of_changes_in_equity, anomalies, is_balanced), generated_at
notesOne row per note, content as structured JSON blocksid, engagement_id, note_key (e.g. note_general, directors_report, note_ppe), title, content (array of {type: "paragraph"|"subheading"|"table", ...}), generated_at
disclosure_responsesUser answers to interrogation promptsid, 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):

  1. Client CRUDPOST/GET/PATCH/DELETE /api/v1/clients/. Frontend pages: Clients.jsx, NewClient.jsx.
  2. Engagement CRUDPOST/GET/PATCH/DELETE /api/v1/engagements/?client_id=.... Frontend: Engagements.jsx, NewEngagement.jsx.
  3. Trial Balance UploadPOST /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 engagement status → tb_uploaded, has_prior_year. Frontend: Upload.jsx with drag-drop and live preview.
  4. 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 (sets is_override=true).
    • POST /api/v1/mapping/{engagement_id}/validate — gap check.
    • Frontend: Mapping.jsx — table with dropdowns, "Auto Suggest" button, confidence badges.
  5. Statements GenerationPOST /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.jsx tabbed view (BS / P&L / SOCE / CF).
  6. Disclosure Interrogation — Rule-engine driven by disclosure_rules.json.
    • GET /api/v1/interrogation/{engagement_id}/gaps — evaluates each rule's trigger_condition (always or tb_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.jsx accordion form with conditional field visibility, progress bar, and regulatory callouts (SFRS sections, Companies Act refs).
  7. Notes & Directors Report GenerationPOST /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 (with headers and rows). A {type: "financial_results_placeholder"} block is replaced by Python-built financial-results table.
    • Upserts to notes. Updates engagement status → 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.
  8. Word Document ExportPOST /api/v1/output/{engagement_id}/word. Returns a StreamingResponse with .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}.
  9. Auth — Stub only in source. Migrate to BreezyCorp staff auth + magic-link portal.
  10. 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, and note_key linkage.

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:

  1. 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.
  2. No XBRL output without passing validation. The export endpoints check XbrlValidationResult for the filing. If any row has severity='error' and passed=false, the export returns HTTP 422.
  3. Every tag mapping must be human-confirmed. If any XbrlTagMapping.confirmedById IS NULL, the export returns HTTP 422. Never auto-confirm.
  4. 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.
  5. 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 tableMigration targetNotes
tenantsDROPReplaced by Client
tenant_membersDROPReplaced by StaffUser + (role, product, action) RBAC
entitiesMerge into Client (already has companyName, uen); add accountingStandard and fyEndMonth if not presentSource has UEN, accounting standard (SFRS / SFRS_SE / IFRS), FY-end month
filingsXbrlFilingNew
source_documentsXbrlSourceDocument (FK to existing File model)Reuse File for S3 storage; XbrlSourceDocument is filing-scoped metadata
line_itemsXbrlLineItemNew
tag_mappingsXbrlTagMappingNew
validation_resultsXbrlValidationResultNew
output_artifactsXbrlOutputArtifact (FK to File)New
audit_logDROPReplaced by existing AuditEvent

Field-level model (target Prisma):

  • XbrlFilingid, 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 to FsEngagement if cross-product), currentVersion, timestamps.
  • XbrlSourceDocumentid, xbrlFilingId, fileId (→ File), sha256, mimeType, parseStatus.
  • XbrlLineItemid, xbrlFilingId, sourceDocumentId, statement (income_statement | balance_sheet | cash_flow | equity_changes | notes), label, valueCurrent (Decimal(20,2)), valuePrior (Decimal(20,2), nullable), sourcePage, sourceLine.
  • XbrlTagMappingid, xbrlFilingId, lineItemId (unique per filing), elementId (taxonomy element id), confidence (HIGH | MEDIUM | LOW | MANUAL), confirmedById (FK StaffUser, nullable), confirmedAt.
  • XbrlValidationResultid, xbrlFilingId, ruleId, severity (ERROR | WARNING | INFO), passed, message, relatedElements (string[]), runAt.
  • XbrlOutputArtifactid, 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):

  1. Document uploadPOST /api/v1/filings/{filing_id}/documents. Multipart DOCX or PDF. Backend extracts tables (DOCX: python-docx paragraphs and tables; PDF: pdfplumber tables → text fallback), normalizes numbers, produces LineItem[] (statement, label, value_current, value_prior, source_page, source_line). Auto-generates MappingSuggestion[] via fuzzy match against mapping_library.json exact synonyms first, then RapidFuzz ratio against taxonomy_acra_2026_v1.json element labels.
  2. Line items listGET /api/v1/filings/{filing_id}/line-items. Returns parsed line items with current confirmed element_id (if any) and current top suggestion + alternatives.
  3. Tag mapping confirmationPOST /api/v1/filings/{filing_id}/mappings/{line_item_id} with {element_id, save_as_synonym?: bool}. Sets confirmedById and confirmedAt. If save_as_synonym is true, appends {label, element_id} to mapping_library.json (writable constitution file — careful: requires audit + restricted to authorized roles).
  4. Tag mapping unconfirmDELETE /api/v1/filings/{filing_id}/mappings/{line_item_id}. Clears confirmedById and confirmedAt.
  5. Synonym addPOST /api/v1/mappings/save-synonym with {label, element_id, scope?}. Direct mutation of mapping_library.json.
  6. Validation runPOST /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 to XbrlValidationResult; returns {errors: [...], warnings: [...], info: [...]}.
  7. BizFinx XLSX exportPOST /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 element period_type (duration → flow statements; instant → balance sheet) and taxonomy_section. Persists to XbrlOutputArtifact; returns download.
  8. HealthGET /health{status: "ok"}. Map to existing /health/live and /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 scripts taxonomy_excel_to_json.py and rules_excel_to_json.py. Port these scripts to TypeScript or keep them as build-time Python tools in packages/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/                       ← unchanged

5.2 Schema changes — cross-cutting

prisma
// 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)

RoleFS ActionsXBRL Actions
FS_PREPARERVIEW_FS_ENGAGEMENT, MANAGE_FS_ENGAGEMENT, UPLOAD_TRIAL_BALANCE, CONFIRM_FS_MAPPING, OVERRIDE_FS_MAPPING, GENERATE_STATEMENTS, RESPOND_DISCLOSURE, GENERATE_NOTES
FS_REVIEWERAll FS_PREPARER + EXPORT_FS_DOCUMENT
XBRL_PREPARERVIEW_FS_ENGAGEMENT (read-only, for cross-product reference)VIEW_XBRL_FILING, MANAGE_XBRL_FILING, INGEST_XBRL_DOCUMENT, CONFIRM_TAG_MAPPING
XBRL_REVIEWERVIEW_FS_ENGAGEMENTAll XBRL_PREPARER + RUN_XBRL_VALIDATION, EXPORT_BIZFINX, EXPORT_XBRL_XML, MARK_XBRL_FILED, ADD_MAPPING_SYNONYM
SENIOR_ACCOUNTANTAll FS actionsAll XBRL actions except MARK_XBRL_FILED (regulator-facing — reserve for XBRL_REVIEWER and PLATFORM_ADMIN)
PLATFORM_ADMINAllAll
CLIENT_APPROVERAPPROVE_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 into FsTrialBalanceItem. Updates engagement status → TB_UPLOADED. Emits FS_TB_UPLOADED audit event.
  • auto-suggest-mapping — payload: {engagementId}. Calls Claude Sonnet with TB items + full SFRS taxonomy; upserts taxonomyKey/taxonomyLabel/mappingConfidence/mappingRationale per row. Updates engagement status → TB_MAPPED if no rows remain unmapped.
  • generate-statements — payload: {engagementId, requestedBy}. Pure deterministic computation (no AI). Builds BS / P&L / SOCE / CF (indirect method via cashflow-engine). Anomaly detection. Persists new FsGeneratedStatement row (versioned). Updates engagement status → STATEMENTS_GENERATED.
  • evaluate-disclosures — payload: {engagementId}. Loads disclosure_rules.json, evaluates each rule's trigger_condition against 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, replaces financial_results_placeholder block with computed table, upserts FsNote rows. Updates engagement status → NOTES_COMPLETE (or NOTES_ERROR on failure with retry).
  • export-statements-docx — payload: {engagementId, generatedStatementVersion?}. Builds DOCX using @breezycorp/financial-statements/output/word. Persists to S3 with financial-statements/<clientId>/<engagementId>/<filename>.docx key. Returns signed URL.

XBRL:

  • ingest-fs-document — payload: {xbrlFilingId, fileId}. Reads DOCX/PDF from S3, computes sha256, persists XbrlSourceDocument. Enqueues extract-line-items.
  • extract-line-items — payload: {sourceDocumentId}. Parses DOCX (via mammoth or docx) or PDF (via pdf-parse + table heuristics), normalizes amounts, persists XbrlLineItem rows. Updates filing status → DOCUMENTS_INGESTED. Enqueues suggest-tag-mappings.
  • suggest-tag-mappings — payload: {xbrlFilingId}. For each XbrlLineItem without a confirmed mapping: exact-match against mapping_library.json; fall back to fuzzy-match (fuse.js or string-similarity) against taxonomy element labels filtered by entry point and statement type. Persists XbrlTagMapping with confirmedById = NULL, alternatives populated. Updates filing status → MAPPING_IN_PROGRESS.
  • run-xbrl-validation — payload: {xbrlFilingId, requestedBy}. Loads all confirmed mappings + taxonomy + rules for taxonomyVersion. Evaluates each rule's expression. Writes one XbrlValidationResult per rule. Updates filing status → VALIDATION_PASSED if no errors, else VALIDATION_FAILED.
  • generate-bizfinx-xlsx — payload: {xbrlFilingId, requestedBy}. Gate: check no error-severity XbrlValidationResult and no XbrlTagMapping with confirmedById 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, write XbrlOutputArtifact. Updates filing status → 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/):

MethodPathActionPurpose
POST/ops/financial-statements/engagementsMANAGE_FS_ENGAGEMENTCreate engagement
GET/ops/financial-statements/engagementsVIEW_FS_ENGAGEMENTList (filter by clientId, status)
GET/ops/financial-statements/engagements/:idVIEW_FS_ENGAGEMENTDetail
PATCH/ops/financial-statements/engagements/:idMANAGE_FS_ENGAGEMENTUpdate (year-end, currency, standard)
DELETE/ops/financial-statements/engagements/:idMANAGE_FS_ENGAGEMENTSoft delete (set status archive)
POST/ops/financial-statements/engagements/:id/trial-balanceUPLOAD_TRIAL_BALANCEUpload TB; enqueues parse-trial-balance
GET/ops/financial-statements/engagements/:id/mappingVIEW_FS_ENGAGEMENTList TB items + mapping state
POST/ops/financial-statements/engagements/:id/mapping/auto-suggestCONFIRM_FS_MAPPINGEnqueue auto-suggest-mapping
PATCH/ops/financial-statements/engagements/:id/mapping/:tbItemIdOVERRIDE_FS_MAPPINGManual override (sets isOverride=true)
POST/ops/financial-statements/engagements/:id/statements/generateGENERATE_STATEMENTSEnqueue generate-statements
GET/ops/financial-statements/engagements/:id/statementsVIEW_FS_ENGAGEMENTLatest FsGeneratedStatement
GET/ops/financial-statements/engagements/:id/statements/:sectionVIEW_FS_ENGAGEMENTSingle section: balance-sheet, income-statement, cash-flow, equity
GET/ops/financial-statements/engagements/:id/disclosures/gapsVIEW_FS_ENGAGEMENTLive evaluation of disclosure_rules.json against mapped TB
POST/ops/financial-statements/engagements/:id/disclosures/respondRESPOND_DISCLOSUREUpsert {ruleId, responses[]}
POST/ops/financial-statements/engagements/:id/notes/auto-generateGENERATE_NOTESEnqueue generate-notes (background)
GET/ops/financial-statements/engagements/:id/notes/statusVIEW_FS_ENGAGEMENTPolling endpoint
GET/ops/financial-statements/engagements/:id/notesVIEW_FS_ENGAGEMENTList notes
POST/ops/financial-statements/engagements/:id/exports/wordEXPORT_FS_DOCUMENTEnqueue + return signed URL

Portal (Financial Statements client review):

MethodPathPurpose
GET/portal/financial-statements/:tokenVerify token; load engagement summary + statements preview
POST/portal/financial-statements/:token/approveClient approves; updates engagement, audit, notify staff
POST/portal/financial-statements/:token/request-revisionClient requests revision with comment

XBRL (apps/api/src/routes/ops/xbrl/):

MethodPathActionPurpose
POST/ops/xbrl/filingsMANAGE_XBRL_FILINGCreate filing (clientId, period, entryPoint, entityType, taxonomyVersion)
GET/ops/xbrl/filingsVIEW_XBRL_FILINGList
GET/ops/xbrl/filings/:idVIEW_XBRL_FILINGDetail
PATCH/ops/xbrl/filings/:idMANAGE_XBRL_FILINGUpdate header (rare)
POST/ops/xbrl/filings/:id/documentsINGEST_XBRL_DOCUMENTMultipart upload; enqueues ingest-fs-document
POST/ops/xbrl/filings/:id/from-fs-engagementINGEST_XBRL_DOCUMENTCross-product: produce line items from FsGeneratedStatement directly (skips DOCX parse)
GET/ops/xbrl/filings/:id/line-itemsVIEW_XBRL_FILINGList line items + mapping state + suggestions
POST/ops/xbrl/filings/:id/mappings/:lineItemIdCONFIRM_TAG_MAPPINGConfirm {elementId, saveAsSynonym?: bool}; if saveAsSynonym requires ADD_MAPPING_SYNONYM
DELETE/ops/xbrl/filings/:id/mappings/:lineItemIdCONFIRM_TAG_MAPPINGUnconfirm
POST/ops/xbrl/mappings/synonymsADD_MAPPING_SYNONYMAppend {label, elementId} to mapping_library.json (audit)
POST/ops/xbrl/filings/:id/validateRUN_XBRL_VALIDATIONEnqueue run-xbrl-validation
GET/ops/xbrl/filings/:id/validation-resultsVIEW_XBRL_FILINGLatest results
POST/ops/xbrl/filings/:id/exports/bizfinxEXPORT_BIZFINXEnqueue generate-bizfinx-xlsx (gated)
POST/ops/xbrl/filings/:id/exports/xbrl-xmlEXPORT_XBRL_XMLEnqueue generate-xbrl-xml (gated)
GET/ops/xbrl/filings/:id/exportsVIEW_XBRL_FILINGList artifacts with download URLs
POST/ops/xbrl/filings/:id/mark-filedMARK_XBRL_FILEDRecords filing reference, status FILED

Portal (XBRL client sign-off):

MethodPathPurpose
GET/portal/xbrl/:tokenVerify token; load filing summary + statement-level previews + validation pass status
POST/portal/xbrl/:token/approveClient signs off; updates filing
POST/portal/xbrl/:token/request-revisionClient requests revision with comment

Admin (extensions to apps/api/src/routes/admin/):

MethodPathActionPurpose
PATCH/admin/clients/:id/fs-configMANAGE_CLIENTSUpdate Client.fsConfig
PATCH/admin/clients/:id/xbrl-configMANAGE_CLIENTSUpdate Client.xbrlConfig
GET/admin/xbrl/taxonomy-versionsMANAGE_TEMPLATESList installed taxonomy versions (read constitution dir)

5.6 Web app — page inventory

Financial Statements (apps/web/src/app/dashboard/financial-statements/):

RoutePagePurpose
/dashboard/financial-statementsEngagementsListTable by client, status, year-end
/dashboard/financial-statements/newNewEngagementForm: client, year-end, reporting standard, currency
/dashboard/financial-statements/[engagementId]EngagementOverviewStage indicators (TB / Mapping / Statements / Disclosures / Notes / Export)
/dashboard/financial-statements/[engagementId]/uploadUploadTBDrag-drop, sample CSV link, parsed-result preview, "Replace upload"
/dashboard/financial-statements/[engagementId]/mappingMappingTable: account code, description, balance, taxonomy dropdown, confidence badge, AI rationale tooltip, "Auto Suggest" button
/dashboard/financial-statements/[engagementId]/statementsStatementsTabs: Balance Sheet, P&L, SOCE, Cash Flow. "Generate" button. Download buttons (Word now, PDF/XLSX later)
/dashboard/financial-statements/[engagementId]/interrogationInterrogationAccordion of triggered rules, conditional fields, progress bar, regulatory callouts (SFRS section, Companies Act ref)
/dashboard/financial-statements/[engagementId]/notesNotesRead-only preview of generated notes + Directors Report; "Regenerate" with confirm

XBRL (apps/web/src/app/dashboard/xbrl/):

RoutePagePurpose
/dashboard/xbrlFilingsListTable by client, period, entry point, status
/dashboard/xbrl/newNewFilingForm: client, period, entry point, entity type, taxonomy version, optional fsEngagementId
/dashboard/xbrl/[filingId]FilingOverviewStage indicators (Documents / Mappings / Validation / Exports)
/dashboard/xbrl/[filingId]/documentsDocumentsUpload DOCX/PDF or "Pull from Financial Statements engagement"
/dashboard/xbrl/[filingId]/mappingsMappingsMaster-detail: line items list with confidence badges + element confirm/override panel; element search; "Save as synonym" for new exact matches
/dashboard/xbrl/[filingId]/validationValidationErrors / Warnings / Info panes; "Validate" button; rule explanations
/dashboard/xbrl/[filingId]/exportsExportsGenerate 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 Revision
  • product === XBRL_FILING → render filing summary + Approve / Request Revision

5.7 Constitution files & loaders

Financial Statements (packages/financial-statements/constitution/):

  • sfrs_taxonomy.json — port verbatim from finstatement/backend/app/data/sfrs_taxonomy.json. Loader: loadSfrsTaxonomy(): SfrsTaxonomy.
  • disclosure_rules.json — port verbatim from finstatement/backend/app/data/disclosure_rules.json. Loader: loadDisclosureRules(): DisclosureRule[].
  • mandatory_policies/*.md — extract verbatim policy text (Income Tax, Employee Benefits, Provisions) currently embedded in finstatement/backend/app/api/notes.py prompt. Expose via getMandatoryPolicies(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. Loader loadTaxonomy(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:

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

  1. 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.
  2. 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:

  1. Bookkeeping → Financial Statements: A button on the FS engagement upload screen — "Pull trial balance from bookkeeping (FY2024)". Aggregates all JournalEntry rows in JournalBatch rows with status=EXPORTED for the period, by accountCode. Skips manual TB upload. (Phase 2 — not v1.)
  2. Financial Statements → XBRL Filing: The POST /ops/xbrl/filings/:id/from-fs-engagement endpoint. Reads the latest FsGeneratedStatement.statementsJson, expands into XbrlLineItem rows, and auto-confirms mappings where the SFRS taxonomy key has a known ACRA element correspondence in mapping_library.json with confidence=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 Revision

XBRL (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 FILED

7. 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 Product enum: FINANCIAL_STATEMENTS, XBRL_FILING.
  • [ ] Extend Role enum: FS_PREPARER, FS_REVIEWER, XBRL_PREPARER, XBRL_REVIEWER.
  • [ ] Extend Action enum (see §5.2 list).
  • [ ] Extend PortalResourceType enum: FS_ENGAGEMENT, XBRL_FILING.
  • [ ] Extend Client model: add nullable fsConfig Json?, xbrlConfig Json?.
  • [ ] Migration: add_fs_xbrl_product_scaffolding.
  • [ ] Add hasProductPermission matrix 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.json verbatim into packages/financial-statements/constitution/. Extract mandatory policy texts from finstatement notes.py into mandatory_policies/*.md.
  • [ ] Schema. Add FsEngagement, FsTrialBalanceItem, FsGeneratedStatement, FsNote, FsDisclosureResponse. Migration add_financial_statements_aggregates.
  • [ ] Domain layer.
    • EngagementService, status machine status-transitions.ts, loadSfrsTaxonomy, loadDisclosureRules, getMandatoryPolicies.
    • TrialBalanceService with smart CSV/XLSX parser (port tb_parser.py logic — column detection, sign normalization, year detection).
    • StatementGenerator (port statements.py BS/PL/SOCE assembly + cashflow_engine.py indirect method).
    • DisclosureRuleEngine (port the tb_contains_any(...) evaluator and rule sorting).
    • MappingService + ClaudeMappingPrompt (port mapping.py).
    • NotesGeneratorService + ClaudeNotesPrompt (port notes.py 16K-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 of documents). 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 from output.py precisely (cover page, header/footer borders, page numbers, financial results table position, dynamic SOCE columns, note auto-numbering). Visual regression against financial_statements_final3.docx.
  • [ ] Portal extension. /portal/[token] route handles product === 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 status TB_MAPPED.
  • [ ] Tests.
    • Domain: status transitions, deterministic statement generation against the test_tb_with_py.csv fixture.
    • 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).
  • [ ] Manual playbook. docs/financial-statements/quick-start.md walking 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.json into packages/xbrl/constitution/. Port the Excel-to-JSON conversion scripts (or document the manual workflow).
  • [ ] Schema. Add XbrlFiling, XbrlSourceDocument, XbrlLineItem, XbrlTagMapping, XbrlValidationResult, XbrlOutputArtifact. Migration add_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 with confirmedBy = NULL.
    • ValidationEngine — rule expression evaluator; rule taxonomy-version-aware.
    • ExportService.bizfinxXlsx, ExportService.xbrlXml — both check validationResults (no error-severity, all passed) AND all tagMappings.confirmedBy != NULL. Rules 2 and 3 enforced as service-layer guards with explicit XbrlExportBlockedError and matching HTTP 422.
    • SynonymStore.append({label, elementId, staffUserId}) — writes mapping_library.json, emits audit event, restricted to ADD_MAPPING_SYNONYM permission.
  • [ ] Worker handlers. All six XBRL handlers (§5.4). The generate-bizfinx-xlsx and generate-xbrl-xml handlers re-check the gate (defense in depth).
  • [ ] API routes. All XBRL endpoints (§5.5) including admin xbrl-config and taxonomy-versions.
  • [ ] Output libraries. exceljs for BizFinx multi-sheet workbook (Income Statement, Balance Sheet, Cash Flow, Equity Changes, Filing Information). xmlbuilder2 for XBRL XML instance with contexts/units/facts.
  • [ ] Portal extension. /portal/[token] handles product === 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_SYNONYM writes are also audited.
  • [ ] Seed data. Add a fifth demo client (STELLAR) — XBRL-only, with one filing in MAPPING_IN_PROGRESS (3 unmapped line items, 5 confirmed) and one in VALIDATION_FAILED (with 2 errors and 5 warnings).
  • [ ] Tests.
    • Constitution-rule enforcement: a unit test that greps packages/xbrl/src/**/*.ts for sg-as_ / sg-dei_ / sg-bp_ literals outside taxonomy/ 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 against test_export.xlsx.
  • [ ] Manual playbook. docs/xbrl/quick-start.md walking 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 from FsGeneratedStatement.statementsJson; auto-confirm mappings where the SFRS key → ACRA element mapping in mapping_library.json is HIGH confidence; leave MEDIUM/LOW unconfirmed for manual review.
  • [ ] Bookkeeping → FS: "Pull trial balance from bookkeeping" button on the FS upload screen. Aggregates JournalEntry (status=APPROVED) by accountCode for 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_FILING is in enabledProducts).
  • [ ] Tests for the cross-product flows.

Phase 4 — Polishing & operational readiness (ongoing)

  • [ ] FS PDF export.
  • [ ] FS XLSX export.
  • [ ] XBRL XML conformance check via arelle sidecar.
  • [ ] Per-client custom mapping libraries (override the global mapping_library.json per clientId).
  • [ ] 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.

  1. No taxonomy literal outside constitution files.
    • [ ] No occurrences of sg-as_, sg-dei_, sg-bp_, sg-fs_, etc. in packages/xbrl/src/ outside taxonomy/loader.ts and taxonomy/lookup.ts.
    • [ ] All element references go through TaxonomyLookup.
  2. No XBRL output without passing validation.
    • [ ] ExportService.bizfinxXlsx and ExportService.xbrlXml check validationResults for any severity = ERROR and passed = false and reject with HTTP 422.
    • [ ] Worker handlers generate-bizfinx-xlsx and generate-xbrl-xml re-check the gate (defense in depth).
  3. Every tag mapping must be human-confirmed.
    • [ ] ExportService rejects if any XbrlTagMapping.confirmedById IS NULL.
    • [ ] No code path sets confirmedById automatically (only in response to the POST .../mappings/:lineItemId endpoint with an authenticated staff user).
  4. Taxonomy version is first-class.
    • [ ] TaxonomyLoader.load(version) is the only entry point; refuses unknown versions.
    • [ ] XbrlFiling.taxonomyVersion is required on creation and immutable thereafter.
    • [ ] All validation rule loading respects the filing's pinned version.
  5. Run regression suite before push.
    • [ ] pnpm --filter @breezycorp/xbrl test passes.
    • [ ] Snapshot test against test_export.xlsx golden output passes.

9. Open Questions / Risks

  1. DOCX-to-XBRL parsing fidelity. The xbrl-saas source uses python-docx + pdfplumber; the TS equivalents (mammoth, pdf-parse) have different table-extraction characteristics. Plan: build with the test fixtures from apps/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.
  2. 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.
  3. 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.
  4. 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.
  5. Multi-tenant constitution writes. mapping_library.json is 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.
  6. 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

Internal use only — BreezyCorp