Skip to content

FS-05 · Disclosure interrogation

SOP: SOP_Financial_Statements.md §6 / Step 5.0Actors: FS Preparer (fs-preparer@spade.local) — holds RESPOND_DISCLOSURE. Pre-state: Engagement at STATEMENTS_GENERATED. Post-state: FsDisclosureResponse rows populated for every triggered mandatory rule (and as many conditional / recommended rules as the engagement requires).

0. Prerequisites

  • Engagement at STATEMENTS_GENERATED (see FS-04).
  • Disclosure rules constitution loaded fine (run pnpm --filter @breezycorp/financial-statements test to confirm).

1. Steps

1.1 Read the live gap list

http
GET /ops/financial-statements/engagements/<engagementId>/disclosures/gaps
Authorization: Bearer <fs-preparer-jwt>

The handler:

  1. Reads every FsTrialBalanceItem.taxonomyKey and builds a Set<string> of present keys.
  2. Calls evaluateDisclosureGaps(presentKeys):
    • Walks every rule in disclosure_rules.json
    • Evaluates each rule's trigger_condition (always or tb_contains_any([...]), joined by " OR ")
    • Returns triggered rules sorted by priority (mandatoryconditionalrecommended), then rule_id
  3. Returns the list, annotated with which prompts already have responses.

1.2 Answer a rule

In the UI: expand the accordion for a rule; fill the prompt inputs; click Save.

Or via API:

http
POST /ops/financial-statements/engagements/<engagementId>/disclosures/respond
Content-Type: application/json
Authorization: Bearer <fs-preparer-jwt>

{
  "ruleId": "DISC-001",
  "responses": [
    { "promptId": "DISC-001-01", "value": "Aurora Holdings Pte. Ltd." },
    { "promptId": "DISC-001-02", "value": "202412345A" },
    { "promptId": "DISC-001-03", "value": "10 Anson Road #20-01, International Plaza, Singapore 079903" },
    { "promptId": "DISC-001-04", "value": "Wholesale trade in industrial machinery and parts" },
    { "promptId": "DISC-001-05", "value": "2024-12-31" }
  ]
}

The handler:

  1. Asserts RESPOND_DISCLOSURE.
  2. Looks up the rule in the constitution; asserts each promptId exists.
  3. Validates each value against the prompt's input_type (text / date / select / boolean / table / multi_text / number).
  4. Upserts one FsDisclosureResponse per (engagementId, ruleId, promptId).
  5. Emits AuditEvent fs.disclosure.responded (one event per rule, payload lists prompt ids and timestamps).

1.3 Conditional prompt visibility

The UI hides any prompt whose visible_when predicate is false. Example: in DISC-003 (Going Concern), the prompt DISC-003-02 ("Describe the exit strategy") is visible_when: "DISC-003-01 == 'no'". If the answer to DISC-003-01 is yes, the exit-strategy prompt is hidden and not required.

2. Verification

Database

sql
SELECT rule_id, count(*) AS prompt_responses
  FROM fs_disclosure_responses
  WHERE engagement_id = '<engagementId>'
  GROUP BY rule_id
  ORDER BY rule_id;

Gap check

http
GET /ops/financial-statements/engagements/<engagementId>/disclosures/gaps

Mandatory rules should have triggered: true, complete: true. Conditional rules whose trigger fired should have complete: true if you intend to ship; otherwise the notes generator will refuse.

Audit log

Event typeNotes
fs.disclosure.respondedOne per save action; payload identifies the rule + prompts

3. Negative & edge cases

  • Unknown ruleId or promptId400 Bad Request with the offending id; safer than silently dropping.
  • Wrong value shape for input_type — e.g. a string passed to a date field. Server-side validation rejects with 400.
  • Reading gaps before mapping is complete — handler returns the gap list but flags presentKeys as partial. Useful for early scoping but should not be relied on for completeness.
  • Mandatory rule unanswered when notes generation is enqueuedgenerate-notes handler refuses (409 Conflict) and returns the list of missing mandatory prompt ids.
  • Reopening a previously-answered prompt — the upsert replaces the value. The audit log captures both the before and after.

Next

Proceed to FS-06 · Generate notes & Directors Report.

Internal use only — BreezyCorp