Technical design — MOD-053 Case & complaint management¶
Module: MOD-053 — Case & complaint management
System: SD08 — Customer App & Back Office Platform
Repo: bank-app
Module type: BFF Lambda + IaC + UI
FR scope: FR-329, FR-330, FR-331, FR-332
NFR scope: NFR-005, NFR-019, NFR-024
Policies satisfied: CON-002 (ALERT), CON-003 (AUTO), REP-001 (LOG)
Author: AI agent (Claude Opus 4.7)
Date: 2026-05-17
Dependencies (all Deployed): MOD-047, MOD-052, MOD-063, MOD-068, MOD-103, MOD-104; consumes MOD-153 events from bank-kyc bus
Objective¶
The full IDR (internal dispute resolution) case lifecycle plus the AML-REFER auto-creation path triggered by MOD-153 acceptance decisions.
- FR-329 — every complaint via any channel becomes a structured
case record with a unique
CAS-{YYYY}-{seq:06d}reference; acknowledgement to the customer within 1 business hour (delivered by MOD-063 from thecase_openedevent MOD-053 emits). - FR-330 — SLA tracking against statutory timeframes (NZ FSCL 40d, AU AFCA 45d, hardship 21d, fraud 14d). 5-day pre-deadline escalation via a daily EventBridge sweeper.
- FR-331 — every case action lands as an immutable row in
app.complaint_events(ADR-048 Cat 1, NFR-024 = 0). - FR-332 —
app.complaint_register_vview aggregates monthly / type / jurisdiction for the Board report.
Policies:
- CON-002 ALERT — SLA escalation cannot be silenced; sweeper
publishes case_sla_at_risk + writes an ESCALATED audit row.
- CON-003 AUTO — vulnerability_flag / vulnerability_notes on
app.cases, surfaced in every back-office projection (static source
scan asserts the projection).
- REP-001 LOG — the register view + immutable event log feed the
regulator-facing complaints report.
Two case classes:
- IDR — customer-channel and external (phone/email/regulator) intake.
- AML-REFER — auto-created from bank.kyc.acceptance_decided
events with decision IN ('REFER', 'HOLD_FOR_EDD').
Architecture¶
Customer app / Back-office console
│
├─ POST /cases createCase (customer-channel, JWT claims)
├─ POST /internal/cases createCaseInternal (IAM-authed back-office + adapters)
├─ GET /cases listCases
├─ GET /cases/{id} getCase
├─ POST /cases/{id}/events addCaseEvent
└─ PATCH /cases/{id}/status updateCaseStatus
bank-kyc bus
└─ bank.kyc.acceptance_decided → consume-acceptance-decided → AML_REFER case
EventBridge daily cron
└─ sla-escalation-sweeper → ESCALATED events + bank.app.case_sla_at_risk
bank-app bus
├─► bank.app.case_opened consumed by MOD-063 (acknowledgement), MOD-064 (work queue)
├─► bank.app.case_status_changed consumed by MOD-063, MOD-074
├─► bank.app.case_sla_at_risk consumed by MOD-063 (manager notification)
└─► bank.app.case_resolved consumed by MOD-063, MOD-074, SD06 analytics
bank-platform bus (orchestrator override #3)
└─► staff.action_taken consumed by MOD-047 → audit.agent_actions
Data plane¶
| V# | Migration | Cat | Notes |
|---|---|---|---|
| V001 | app.cases + app.case_reference_sequence |
mutable | Bare-uuid soft FKs on related_account_id / related_payment_id (ratification #1 — wiki schema had REFERENCES which contradicted the cross-domain UUID convention). vulnerability_flag / vulnerability_notes columns added per #2 (CON-003 AUTO). case_reference_sequence(year PK, seq) enables atomic per-year increment via INSERT … ON CONFLICT DO UPDATE (ratification #8). Resolution + closure CHECKs ensure a non-empty resolution_summary and resolved_at on terminal transitions. |
| V002 | app.complaint_events |
ADR-048 Cat 1 immutable | Trigger trg_complaint_events_immutable rejects UPDATE/DELETE/TRUNCATE. CHECK: STATUS_CHANGED requires both prev/new status; NOTE_ADDED requires a non-empty note. Event-type enum extends the wiki version with ACCEPTANCE_REFERRED for the AML_REFER path. |
| V003 | app.complaint_register_v view |
view | REP-001 LOG. Monthly / type / jurisdiction aggregation. Idempotent GRANT to reports_app_user if the role exists. |
| V004 | access.role_permissions seed |
additive | m8 matrix: compliance/senior = *, operations = IDR types only (NO AML_REFER), customer-facing = no rows. ON CONFLICT DO NOTHING for idempotency. |
SSM outputs¶
Consumed:
- /bank/{env}/iam/lambda/bank-app/arn
- /bank/{env}/eventbridge/bank-app/arn
- /bank/{env}/eventbridge/bank-platform/arn (orchestrator override #3 — staff.action_taken target)
- /bank/{env}/eventbridge/bank-kyc/arn (consumer rule)
- /bank/{env}/eventbridge/bank-kyc/dlq-arn
- /bank/{env}/kms/operational/arn
- /bank/{env}/observability/adot-layer-arn
- ADR-064 DATABASE_URL injection
Published (/bank/{env}/mod053/...): see infra/ssm-outputs.ts.
EventBridge¶
| Direction | DetailType | Bus |
|---|---|---|
| publish | case_opened |
bank-app |
| publish | case_status_changed |
bank-app |
| publish | case_sla_at_risk |
bank-app |
| publish | case_resolved |
bank-app |
| publish | staff.action_taken |
bank-platform (override #3) |
| consume | bank.kyc.acceptance_decided filtered decision IN ('REFER','HOLD_FOR_EDD') |
bank-kyc |
Cross-bus IAM grants: bank-kyc bus subscription needs
events:PutRule/events:PutTargets for BankAppRole on the bank-kyc bus
— filed via docs/handoffs/MOD-053-acceptance-decided-cross-bus-grant.handoff.md
targeting bank-platform (the cross-bus grant owner — orchestrator
correction; not MOD-104).
Policy satisfaction¶
| Policy | Mode | Mechanism | Tests |
|---|---|---|---|
| CON-002 | ALERT | Daily EventBridge cron → sla-escalation-sweeper → ESCALATED audit row + case_sla_at_risk event. Source scan rejects bypass tokens. |
tests/policy/con-002-alert-static.test.ts |
| CON-003 | AUTO | vulnerability_flag / vulnerability_notes on app.cases; every back-office case projection includes the flag; source scan asserts the projection. |
tests/policy/con-003-auto-static.test.ts |
| REP-001 | LOG | app.complaint_register_v view + immutable app.complaint_events. JSON GET endpoint in v1 (PDF deferred — #7). |
tests/policy/rep-001-log-static.test.ts |
Open items¶
- PDF rendering for FR-332: deferred to a MOD-113-style follow-up module per ratification #7. v1 ships the SQL view + JSON endpoint.
- MOD-153 follow-up handoff: MOD-153 currently emits an SNS stub
to the MOD-076 alarm-intake topic while MOD-053's consumer rule was
in flight. Once
consume-acceptance-decidedis live in dev, file a follow-up handoff to bank-kyc to remove the SNS stub.
Cross-module handoffs filed¶
docs/handoffs/MOD-053-complete.handoff.md— wiki amendments (4 items: schema corrections, vulnerability columns, deps update, complaint-events enum widening).docs/handoffs/MOD-053-acceptance-decided-cross-bus-grant.handoff.md— request bank-platform to grant BankAppRole the EventBridge management actions on the bank-kyc bus ARN, scoped toacceptance_decided.
Deployment¶
GitLab CI .gitlab/ci/mod-053.gitlab-ci.yml extends .lambda-mr and
.lambda-deploy from bank-platform. HAS_POSTGRES: "true" triggers
flyway validate + migrate against the consolidated bank DB.
Contract package @bank-app/mod-053-contracts@1.0.0 is published by
the appended mod-053-publish-contracts job whenever contract/**
changes (ADR-063).