Skip to content

X-05 · Export BizFinx XLSX and XBRL XML

SOP: SOP_XBRL_Filing.md §6 / Step 5.0 (VALIDATION_PASSED → EXPORTED)Actors: XBRL Reviewer (xbrl-reviewer@spade.local), Senior Accountant, or Platform Admin — EXPORT_BIZFINX / EXPORT_XBRL_XML. Pre-state: Filing at VALIDATION_PASSED. Every mapping confirmed (asserted by the export gate). No ERROR-severity unpassed validation result (asserted by the export gate). Post-state: XbrlOutputArtifact row inserted. Filing at EXPORTED (first time only; subsequent exports add new artifact versions but the status remains EXPORTED).

0. Prerequisites

  • Filing at VALIDATION_PASSED (see X-04).
  • Caller holds EXPORT_BIZFINX for the XLSX path, EXPORT_XBRL_XML for the XML path. XBRL_REVIEWER and SENIOR_ACCOUNTANT hold both; XBRL_PREPARER holds neither.

1. Steps

1.1 Generate BizFinx XLSX

http
POST /ops/xbrl/filings/<filingId>/exports/bizfinx
Authorization: Bearer <xbrl-reviewer-jwt>

(Or click Generate BizFinx on the Exports tab.)

The handler:

  1. Asserts EXPORT_BIZFINX.

  2. Loads the filing's confirmed mappings, validation results, and line item count.

  3. Calls assertExportAllowed() (the export gate). Four possible blocks:

    • NO_LINE_ITEMS (filing has no line items)
    • NO_VALIDATION_RUN (no validation results exist)
    • VALIDATION_ERROR_PRESENT (severity = ERROR AND passed = false row exists)
    • UNCONFIRMED_TAG_MAPPING (confirmedById IS NULL row exists)

    On any block: the handler raises XbrlExportBlockedError(reason, message, details). The HTTP layer translates this to 422 Unprocessable Entity with the typed reason. No artifact is produced.

  4. On pass: enqueues generate-bizfinx-xlsx with {xbrlFilingId, requestedBy}. Returns 202 Accepted.

1.2 Worker — generate-bizfinx-xlsx

The worker re-runs assertExportAllowed() before writing any bytes (defence in depth). On pass:

  1. Builds a multi-sheet exceljs workbook:
    • Income Statementperiod_type = duration, P&L section
    • Balance Sheetperiod_type = instant, SOFP section
    • Cash Flowperiod_type = duration, SOCF section
    • Equity Changes — SOCE section
    • Filing Informationsg-dei_* elements (name, UEN, period, entry point)
  2. Persists to S3 at xbrl/<clientId>/<filingId>/bizfinx-v<n>.xlsx via the File model with fileKind = XBRL_BIZFINX_EXPORT.
  3. Inserts XbrlOutputArtifact row {kind: 'BIZFINX_XLSX', fileId, versionNo: <next>, generatedAt}.
  4. Transitions filing VALIDATION_PASSED → EXPORTED (first time only).
  5. Emits AuditEvent xbrl.export.generated.

1.3 Generate XBRL XML

http
POST /ops/xbrl/filings/<filingId>/exports/xbrl-xml
Authorization: Bearer <xbrl-reviewer-jwt>

Same gate. The worker generate-xbrl-xml builds an XBRL instance document:

  • Root <xbrl> with namespace declarations for the pinned taxonomy version
  • One <context> per (period, entity)<instant> for balance-sheet elements, <startDate>/<endDate> for flow
  • One <unit> for SGD (<measure>iso4217:SGD</measure>)
  • One element fact per confirmed mapping with contextRef + unitRef (for monetary types)

Persists to S3 at xbrl/<clientId>/<filingId>/xbrl-v<n>.xml. Inserts XbrlOutputArtifact row {kind: 'XBRL_XML', ...}.

2. Verification

Database

sql
SELECT kind, version_no, generated_at FROM xbrl_output_artifacts
  WHERE xbrl_filing_id = '<filingId>' ORDER BY generated_at DESC;
sql
SELECT status FROM xbrl_filings WHERE id = '<filingId>';
-- expect 'EXPORTED'

S3

bash
docker exec breezycorp-monorepo-minio-1 mc ls local/breezycorp/xbrl/<clientId>/<filingId>/
# expect bizfinx-v<n>.xlsx and/or xbrl-v<n>.xml

Audit log

Event typeNotes
xbrl.export.requestedAPI hit
xbrl.export.blockedGate refused (if applicable); payload contains the typed reason
xbrl.export.generatedBackground success; payload includes kind + versionNo + signed URL

Exercise the gate against seed data (manual)

bash
node --experimental-repl-await
js
const { PrismaClient } = await import('@prisma/client');
const xbrl = await import('@breezycorp/xbrl');
const prisma = new PrismaClient();

// STELLAR's MAPPING_IN_PROGRESS filing — should refuse with UNCONFIRMED_TAG_MAPPING
const filing = await prisma.xbrlFiling.findFirst({
  where: { status: 'MAPPING_IN_PROGRESS' },
  include: { tagMappings: true, validationResults: true, lineItems: { select: { id: true } } },
});
try {
  xbrl.assertExportAllowed({
    validationResults: filing.validationResults,
    tagMappings: filing.tagMappings.map((m) => ({ lineItemId: m.lineItemId, confirmedById: m.confirmedById })),
    hasLineItems: filing.lineItems.length > 0,
  });
} catch (e) {
  console.log(e.reason, '—', e.message);
}
// → NO_VALIDATION_RUN — Validation has not been run for this filing.

3. Negative & edge cases

  • UNCONFIRMED_TAG_MAPPING — HTTP 422 with details.lineItemIds. Open the Mappings tab; filter for unconfirmed; confirm each.
  • VALIDATION_ERROR_PRESENT — HTTP 422 with details.count. Open the Validation tab; address each ERROR-severity row; re-run validation.
  • NO_VALIDATION_RUN — HTTP 422. Run validation first.
  • NO_LINE_ITEMS — HTTP 422. Re-upload the source document.
  • Caller without EXPORT_BIZFINX403 Forbidden. XBRL_PREPARER is rejected.
  • Re-export — allowed; each call produces a new artifact with versionNo bumped. The status stays at EXPORTED.
  • Validation result becomes stale — if a mapping is changed after VALIDATION_PASSED, the filing implicitly returns to MAPPING_IN_PROGRESS. Re-validate.
  • Gate bypass via worker direct invocation — the worker re-runs assertExportAllowed(). Defence in depth: there is no path that produces an artifact without the gate.

Next

Proceed to X-06 · Client portal sign-off (optional) or directly to X-07 · Mark filed with ACRA.

Internal use only — BreezyCorp