Skip to content

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 the case_opened event 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-332app.complaint_register_v view 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 AUTOvulnerability_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-decided is live in dev, file a follow-up handoff to bank-kyc to remove the SNS stub.

Cross-module handoffs filed

  1. docs/handoffs/MOD-053-complete.handoff.md — wiki amendments (4 items: schema corrections, vulnerability columns, deps update, complaint-events enum widening).
  2. 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 to acceptance_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).