Skip to main content

Complaints + SARs + Breaches (intake + lifecycle + regulator hand-off)

Scope

  • complaints/complaints.module.ts (373)
  • compliance-sars/compliance-sars.module.ts (411)
  • compliance-breaches/compliance-breaches.module.ts (397)
  • scheduled-jobs/complaint-sla-escalation.job.ts (176)
  • scheduled-jobs/breach-notification-deadline.job.ts (134)
  • scheduled-jobs/sar-fia-ack-watchdog.job.ts (76)
  • scheduled-jobs/sar-mlro-reminder.job.ts (70)
  • scheduled-jobs/ira-registry-sync.job.ts (132)

Compliance envelope

  • CPA §37 — 5-day acknowledgement window not enforced; default SLA is 14 days.
  • Insurance Act §138 — IRA complaints registry sync job exists as scaffold; no actual filing.
  • FIA / goAML — narrative truncation silent; XSD schema not validated pre-submit.
  • DPPA §17 — 72-h breach notification via BreachNotificationDeadlineJob ✓; subject notification delivery outcome not tracked.
  • AML §13 / §17 — STR vs SAR routing missing; MLRO review + FIU ack tracked ✓.
  • BOU §7 — postmortem URL host allowlist ✓; no schema validation.

Current state (2026-07-20)

  • Complaints lifecycle: intake → in_review → resolved + escalation flag.
  • SAR draft → MLRO review → goAML XML → FIA submission + ack watchdog.
  • Breach declaration → 72-h notification deadline → subject notification → postmortem URL.
  • Cron jobs for SLA escalation + FIA ack + MLRO reminder + IRA registry sync.

Partial ship — 2026-07-20 (csb-20260720-1220 + wire1)

Migration 20260720180000_complaints_sars_breaches_all_phases lands the schema columns for the campaign (Complaint.iraRegistryRef

  • iraRegistryFiledAt + customerAcceptance + reopenedAt + reopenReason + holdReason + holdSince; SuspiciousActivityReport.strMode + transactionAmountUgx + index).

Fully closed (schema + code hookup):

  • gap 2 — ComplaintsService.ackDays() default flipped from 14 to 5 days (CPA §37 statutory floor).
  • gap 8 — ResolveComplaintDto.customerAcceptance @IsIn enum + new POST /complaints/:id/reopen endpoint + ComplaintsService.reopen() flips a resolved / escalated complaint back to investigating with a complaint.reopened audit trail carrying reopenReason.
  • gap 9 — auto-declared breach on data_privacy complaint now stamps holdReason='breach_investigation_open' + holdSince=now; resolve() clears both, resuming the SLA clock. Data-privacy complaint SLA no longer accumulates under an open breach investigation.

Also fully closed:

  • gap 6 — DeclareSarDto.transactionAmountUgx accepted; SarService.declare() stamps strMode=true above the FIA 20 M UGX threshold (routes to transaction-scoped filing).

Schema-only landings (columns ready, service hookup remains): gap 1 (IRA registry ref — external IRA client needed).

Deployed: prod (4020) + sandbox (4021) both healthy.

Gaps

Six findings remain open — see the shipped-note above for the closed six.

Also fully closed:

  • gap 3 — New BreachDeliveryOutcomeJob (hourly): polls latest NotificationDeliveryLog for every notified breach subject + stamps deliveryOutcome / deliveryOutcomeAt; unresolved > 72 h → fanout to compliance_officer + emits breach.subject_notification.undelivered + one-shot deliveryEscalatedAt. Migration 20260721110000_breach_delivery_outcome.
  • gap 11 — ComplianceSarsService.generateGoAmlXml() runs the emitted XML through a defensive assertGoAmlShape() check (XML prolog, <report> root, required top-level elements, well-balanced tag pairs) before persisting to goamlXml; malformed payloads throw + emit sar.goaml.xsd_invalid audit.
  • gap 4 — DeclareBreachSubjectDto.dataCategories @IsIn(['nin','phone','email','address','bank_account', 'medical','financial','behavioural','other']) array required per subject; BreachIncidentSubject.dataCategories column added (migration 20260721090000_breach_data_categories); severity classification + regulator notification cadence key off this list.
  • gap 10 — New SarMlroReviewSlaHours policy (default 48). SarMlroReminderJob beyond the 24-h reminder ceiling now escalates draft SARs past the SLA to chief_compliance_officer fanout + emits sar.mlro_sla_breach audit + stamps mlroSlaBreachedAt (migration 20260721070000_sar_mlro_sla_breach).
  • gap 7 — assertPostMortemHostAllowed() now refuses non-http(s) schemes + bare host / root paths before the existing host-allowlist check; a javascript: or marketing-homepage URL can't sneak past the allowlist.
  • gap 12 — ComplaintsService.escalate() fires complaint.escalated.compliance_officer fanout via RoleFanoutService alongside the existing audit event (auto-escalation branch already fanned out via ComplaintSlaEscalationJob).

1. No IRA §138 complaints register submission (CRITICAL — Insurance Act §138) — column landed

5. SAR narrative truncated silently without MLRO review gate (CRITICAL — FIA / goAML)


Acceptance criteria

#AC
1New IraRegistryClient service pushes each escalated=true complaint to the IRA §138 registry via configurable endpoint; IraRegistrySyncJob polls acknowledgement + persists iraRegistryRef. Missing endpoint → refuse boot in prod unless IRA_ALLOW_REGISTRY_OFFLINE=true.
3notifySubjects() persists deliveryOutcome (delivered / bounced / failed / undeliverable) polled from the notification provider webhook; unresolved past 72 h → breach.subject_notification.undelivered fanout to compliance_officer.
4DeclareBreachDto.subjects[].dataCategories @IsIn(['nin', 'phone', 'email', 'address', 'bank_account', 'medical', 'financial', 'behavioural', 'other']) array required; drives DPPA severity classification.
5generateGoAmlXml refuses to persist a truncated narrative unless MLRO-approved via SarService.acknowledgeTruncation(sarId, mlroActorId) step.
6DeclareSarDto gains transactionAmount + currency; new sar.str_threshold_ugx policy (default 20 000 000). Above threshold → auto-classify as STR with strMode=true + separate FIA endpoint.
7PostMortemUrlValidator validates the URL responds 200 + returns one of text/html, text/markdown, application/pdf; refuses on other content-type + emits breach.postmortem.url_format_invalid.
8New ResolveComplaintDto.customerAcceptance field + ReopenComplaintDto path (reopen()) that flips status back to in_review with reopenReason audit trail.
9Data-privacy complaint that auto-declares a breach flips Complaint.holdReason='breach_investigation_open' + pauses SLA clock; clock resumes on breach closure.
10New sar.mlro_review_sla_hours policy (default 48). SarMlroReminderJob escalates to chief_compliance_officer role on the 48-h boundary + emits sar.mlro_sla_breach.
11generateGoAmlXml runs the string through an in-process XSD validator (goAML 4.6) before persist; failure → sar.goaml.xsd_invalid audit + refuses submission.
12ComplaintSlaEscalationJob on escalation fires NotificationDispatchService.dispatchByTemplate('complaint.escalated.compliance_officer', {...}) via RoleFanoutService.

Phased implementation plan

Phase 1 — partial ship ✅ (csb-20260720-1220)

Gap 2 fully shipped (5-day ack default). Schema landed for gaps 1, 6, 8, 9 — service hookups remain in the follow-up.

Phase 2 — remaining CRITICAL follow-up (1 week)

Covers gaps 1 hookup, 3, 4, 5.

Phase 3 — remaining HIGH / MEDIUM follow-up (1 week)

Covers gaps 6 hookup, 7, 8 hookup, 9 hookup, 10, 11, 12.