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_recorded — decision_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_recordedevents. 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_configurationsvia 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_disclosuresrow. CW Logs is the secondary SIEM feed; MOD-048 ledger is the cross-domain audit feed. Page on Postgres insert failures, not ondecision_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.