Skip to content

MOD-050 — Disclosure enforcement (technical design)

Module: MOD-050 System: SD08 (bank-app) Repo: bank-app FR scope: FR-317, FR-318, FR-319, FR-320, FR-661, FR-662, FR-663, FR-664 NFR scope: NFR-010, NFR-011, NFR-024 Policies satisfied: CON-004 (GATE), CRE-002 (GATE), PAY-004 (GATE), CON-005 (GATE), CON-009 (GATE) Dependencies: MOD-068 (Built — JWT issuer), MOD-048 (Built — system_decisions ledger), MOD-103 (Built), MOD-104 (Built) Feature dependency (UI): MOD-069 customer app shell — not yet built; FR-672-style UI patterns deferred Date: 2026-05-08


Objective

Platform-wide pre-acceptance disclosure GATE. Every product acceptance, payment confirmation, fee-generating action, and (NZ-only) deposit account opening is blocked until the customer acknowledges the required disclosure — enforced at the service layer, not the UI. Owns four Postgres tables (app.disclosures version registry, app.disclosure_acks ack ledger, app.dcs_fcs_disclosures NZ KIS ack ledger, app.product_configurations stub) and exposes the canonical requireDisclosure() + requireKisAck() workspace library. Every gate pass posts to MOD-048's system_decisions ledger via the bank-platform bus.

AD-12 (binding): "This is the only authorised implementation of CON-004, CRE-002, PAY-004, CON-005, CON-009 GATE modes." All product flows must import from @bank/disclosure-enforcement — no ad-hoc disclosure logic in product modules.

Architectural decisions (scope review, ratified 2026-05-08)

AD Ruling
1 MOD-050 owns app.dcs_fcs_disclosures (DCS/FCS regime is CCCFA, separate from MOD-049 CDR)
2 MOD-104 cross-bus IAM handoff filed: BankAppRole needs events:PutEvents on bank-platform bus (MOD-024 precedent)
3 Four MOD-048 decision_type values: DISCLOSURE_GATE_PASSED, DISCLOSURE_GATE_BLOCKED, KIS_ACK_RECORDED, DISCLOSURE_VERSION_PINNED. Only PASSED + KIS_ACK_RECORDED post to MOD-048 — BLOCKED is a transient gate state, not a system decision
4 app.product_configurations stub for MOD-127 (forward ownership, ALTER on MOD-127's V1)
5 PAY-004 FX rate-lock IS in v1 scope; MOD-050 records the rate as disclosure_context JSONB; rate-lock window expiry enforcement is MOD-022/MOD-025's concern
6 requireDisclosure(productId, version?) — version optional; supplied stale version blocks with DISCLOSURE_VERSION_STALE
7 Disclosure document content in S3 (15-min presigned URLs); app.disclosures stores only metadata
8 Lambda middleware required in v1; React hooks deferred (no MOD-069 yet)
9 No periodic reminder cadence. KIS triggers: (1) application acceptance gate, (2) MOD-065 material variation event (handoff filed), (3) on-request (out of v1)
10 NZ-only KIS gate; AU returns PASS / NOT_APPLICABLE_JURISDICTION; UNKNOWN blocks with JURISDICTION_INDETERMINATE
11 Idempotency keys: disc-ack:{customer}:{product}:{version}, kis-ack:{customer}:{application}, disc-check:{customer}:{product}:{request_idempotency_key}
12 @bank/disclosure-enforcement workspace package — sole authority for CON-004/CRE-002/PAY-004/CON-005/CON-009 GATEs
13 MOD-068 hard dependency (JWT carries customer_id + jurisdiction + session_id)
14 CON-007/AML-010 wiki MD discrepancies already fixed

Stacks

MOD-050-disclosure-enforcement/
├── infra/
│   ├── functions.ts          9 Lambdas (HTTP only — no scheduled cron per AD-9), x86_64
│   ├── api.ts                HTTP API v2 with the 9 routes
│   ├── audit-log.ts          /aws/bank-app/disclosure-events-{env}, 90d hot
│   ├── ssm-outputs.ts        13 SSM parameters
│   └── index.ts
├── src/
│   ├── lib/
│   │   ├── require-disclosure.ts  ★ canonical CON-004/CRE-002/PAY-004/CON-005 + CON-009 GATE
│   │   ├── kis-template.ts        FR-661 NZ statutory wording + content-addressed renderer
│   │   ├── decision-publisher.ts  cross-bus PutEvents to MOD-048 (AD-2 stop-gap)
│   │   ├── presigned-url.ts       15-min S3 presigned URLs (AD-7)
│   │   ├── idempotency.ts         AD-11 deterministic keys
│   │   ├── errors.ts, logger.ts, trace.ts, db.ts
│   ├── services/
│   │   ├── disclosure-service.ts
│   │   ├── kis-service.ts
│   │   └── event-publisher.ts
│   ├── handlers/
│   │   ├── _shared.ts
│   │   ├── present-disclosure.ts        POST /disclosures/present
│   │   ├── acknowledge-disclosure.ts    POST /disclosures/{id}/acknowledge
│   │   ├── decline-disclosure.ts        POST /disclosures/{id}/decline
│   │   ├── list-disclosures.ts          GET  /disclosures
│   │   ├── check-disclosure.ts          POST /disclosures/check (HTTP shim around requireDisclosure)
│   │   ├── generate-kis.ts              POST /kis/generate
│   │   ├── present-kis.ts               POST /kis/present
│   │   ├── acknowledge-kis.ts           POST /kis/{application_id}/acknowledge
│   │   └── get-current-kis.ts           GET  /kis/{product_id}/current
│   └── index.ts                         workspace exports
├── db/migrations/V001..V004.sql + rollbacks
└── tests/  unit ≥80% (94% achieved), contract, policy (static), integration FR/policy/infra

Data model

Table Purpose Mutability
app.disclosures Document version registry (per AD-7); S3 key + sha256 + effective_from / superseded_at Mutable (status flips on supersession)
app.disclosure_acks Customer ack ledger; one row per (customer × disclosure × outcome) Immutable (ADR-048 Cat 1, FR-318 7y)
app.dcs_fcs_disclosures NZ KIS ack ledger; one row per (customer × application) Immutable (ADR-048 Cat 1, FR-661 7y)
app.product_configurations Stub for MOD-127; required_disclosure_ids + kis_required + rate_lock_minutes Mutable until MOD-127 takes ownership

SSM outputs

Path Value
/bank/{env}/mod050/disclosure-api/url API Gateway URL
/bank/{env}/mod050/disclosure-api/id API Gateway ID
/bank/{env}/mod050/{handler}/fn-arn Each of 9 Lambda ARNs
/bank/{env}/mod050/disclosure-events-log/group-{arn,name} CW Logs group

Events

bank-app bus: - bank.app.disclosure_acknowledged — FR-320 (already in CLAUDE.md events table) - bank.app.disclosure_declined — FR-320 - bank.app.kis_version_published — FR-662 surface refresh signal

bank-platform bus (cross-bus, MOD-048 contract): - bank-platform.system_decision_recordeddecision_type ∈ {DISCLOSURE_GATE_PASSED, KIS_ACK_RECORDED} per AD-3

Test evidence

pnpm test:unit
 76 tests passed in 10 files
 Coverage: ~94% lines/stmts; ~88% branches; 100% functions
FR / Policy Pass
FR-317 mandatory presentation before submission
FR-318 versioned + 7y retention ✓ (ADR-048 Cat 1 trigger)
FR-319 block + machine-readable error (AD-12 envelope) ✓ live
FR-320 every event recorded
FR-661 NZ KIS generated + persisted in dcs_fcs_disclosures ✓ live
FR-662 content-addressed kis_version_id ✓ unit
FR-663 persistent KIS access partial-met (BFF complete; UI follows MOD-069 + MOD-077)
FR-664 jurisdiction-aware NZ-only template
CON-004/CRE-002/PAY-004/CON-005 GATE ✓ static + live
CON-009 GATE NZ-only ✓ static + live
NFR-024 audit log immutability ✓ live
Coverage gate ≥80% ✓ (94%)

Cross-module dependencies + handoffs

  • MOD-068 — JWT issuer; supplies custom:party_id, custom:jurisdiction, custom:session_id. Hard dependency.
  • MOD-048 — system_decisions ledger; consumes bank-platform.system_decision_recorded events. Cross-bus IAM grant pending — see MOD-104-bank-platform-cross-bus-grant-mod050.handoff.md.
  • MOD-127 — product configuration; will assume ownership of app.product_configurations via ALTER migration. Stub schema documented above.
  • MOD-065 — material variation re-disclosure trigger — see MOD-065-material-variation-redisclosure.handoff.md.
  • MOD-104/aws/bank-app/* CW Logs grant (existing handoff covers this).

Operational notes

  • Audit hierarchy: the durable compliance record is the Postgres app.disclosure_acks / app.dcs_fcs_disclosures row. CW Logs is the secondary SIEM feed; MOD-048 ledger is the cross-domain audit feed. Page on Postgres insert failures, not on decision_publish_failed (that's the AD-2 cross-bus stop-gap).
  • No reminder cadence (AD-9): KIS re-presentation triggers on application gate, material variation event, or customer request only. Do NOT add a scheduled reminder cron.
  • NZ-only KIS (AD-10): AU short-circuits with PASS / NOT_APPLICABLE_JURISDICTION; UNKNOWN blocks with JURISDICTION_INDETERMINATE.
  • CDR vs DCS/FCS separation (AD-1): MOD-049 owns CDR (CCCFA Open Banking); MOD-050 owns DCS/FCS (CCCFA Consumer Credit). Two separate regimes, two separate tables, no shared write paths.