Skip to main content

Claims (file → acknowledge → decide → pay lifecycle)

Scope

One monolith service+controller+DTOs at 1 912 lines, plus a 152-line external-services registry. The state machine is correct — tight FSM with a transition matrix, immutable ClaimStatusHistory per transition, ClaimReserveHistory on reserve moves, sanctions screening on payment recording, name- match on beneficiary, HMAC-SHA256 on payeeNameHash for goAML minimisation. Gaps concentrate in policy defaults (SLA default 14 d not 5 d), PII at rest (narrative plaintext), payout integrity (no four-eyes + no rescreen at disburse-time + no idempotency on disburse), DSAR cascade, and throttling.

  • src/modules/claims/claims.module.ts (1 912 lines) — ClaimsService (file / acknowledge / awaitDocs / settle / repudiate / disburse / appeal / close)
    • 30-endpoint controller + DTOs.
  • src/modules/claims/external-services.ts (152 lines).
  • Prisma models: Claim, ClaimStatusHistory, ClaimReserveHistory, ClaimDocument, ClaimPayment, ClaimNote.
  • State machine: filed → under_review ⇄ investigation / awaiting_docs → settled / repudiated → (appealed) → closed; withdrawn loops back to closed.
  • ClaimSlaEscalationJob — hourly sweep flags claims past the acknowledge / decision windows.

Compliance envelope

  • Insurance Act 2017 §138 — 5-day acknowledgement, 30-day decision, 15-day-post-decision payment. Today's default ack-SLA is 14 d (env-overridable) — 9 days off statutory floor.
  • CPA 2022 §37 — consumer notice + plain-language reasons. Notification dispatch wired ✓; reason-plainness unenforced.
  • DPPA 2019 §21Claim.description plaintext (500 char max). PiiCipher injected but never applied.
  • DPPA 2019 §11ClaimDocument.retentionUntil exists; Claim itself has no retention shape.
  • DPPA 2019 §17 — DSAR erasure doesn't cascade to narrative / notes / payee.
  • AML §14 — state-transition audits present but narrative
    • repudiation-reason bodies absent from the audit payload.
  • AML §17 — sanctions rescreen runs at record-time not at disburse-time.
  • IRA IT Controls §1.2 — no four-eyes gate on high-value payouts (only @RequireStepUp(15)).
  • BOU cybersecurity §5.4 — no @Throttle on any claims mutation endpoint.

Current state (2026-07-20)

What works today

  • Tight state machine + FSM enforcement in transitionTo().
  • Append-only ClaimStatusHistory per transition.
  • ClaimSlaEscalationJob breach detection.
  • SanctionsService.screenForPayout at recordPayment().
  • Beneficiary name-match on the payout provider path.
  • payeeNameHash HMAC-SHA256 sibling for goAML minimisation.
  • Idempotency on POST /claims via fileIdempotent().
  • Consumer notification dispatch on major transitions.
  • Public PIN lookup uses constant-time HMAC comparison.
  • Fine-grained ClaimFile / ClaimSettle / ClaimDisburse / ClaimAppeal permissions.

Phase 3 shipped — 2026-07-20 (claims-p3-20260720-0740)

Migration 20260720050000_claims_phase3 adds Claim.retentionUntil, retentionScrubbedAt, deletedAt + partial index on retentionUntil. RepudiateClaimDto.reason gains @Length(20, 2000) + @Matches(...) regex rejecting underscore/punctuation-only jargon (CPA §37). close() stamps retentionUntil = closedAt + <policy days> (default 2555 = 7 y via new data_sharing.claims_retention_days policy). RetentionPurgeJob.tick sweeps rows past the window, soft- deleting + scrubbing narrative + audit-recording claim.retention.purged. settle / repudiate / appeal / close accept X-Idempotency-Key via new transitionIdempotent() wrapper. file() emits claim.insurer_notification.dispatched audit event when policy.insurerId is set — hook point for a future outbound insurer-webhook subscriber.

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/claims-phase3.e2e-spec.ts (2 tests, both pass). Full suite green: 104 suites / 698 tests.

Claims module — ALL 12 GAPS CLOSED ✅

Phase 2 shipped — 2026-07-20 (claims-p2-20260720-0731)

Migration 20260720040000_claims_phase2 adds ClaimDisbursementApproval (with partial unique index for consumedAt IS NULL). New policy code claims.four_eyes_threshold_ugx (default 5 000 000) drives ClaimsService.fourEyesThresholdUgx(). Above threshold, disburse() refuses without a distinct-actor approval; the approval row is consumed atomically. New endpoint POST /claims/:id/approve-disbursement (ClaimDisburseApprove permission). repudiate() writes an audit event carrying repudiatedReasonHash + repudiatedReasonPrefix. @Throttle added to POST /claims (10/min), settle/repudiate/appeal/close (20/min), disburse (5/min), approve-disbursement (10/min). DsarService.eraseClient now cascades to claim rows: scrubs Claim.description[ERASED], descriptionEncrypted + descriptionHash → null, repudiatedReason → null, ClaimNote.body[ERASED], ClaimPayment.payeeName[ERASED].

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/claims-phase2.e2e-spec.ts (2 tests, both pass). Full suite green: 104 suites / 698 tests.

Phase 1 shipped — 2026-07-20 (claims-p1-20260720-0702)

Migration 20260720030000_claims_phase1 adds Claim.descriptionEncrypted + descriptionHash + @@index([descriptionHash]). New policy code claims.acknowledgement_sla_days (default 5) drives ClaimsService.ackDays() with CLAIM_ACK_SLA_DAYS env as legacy fallback. POST /claims/:id/disburse accepts X-Idempotency-Key via disburseIdempotent() scoped per claim id; body mismatch → IDEMPOTENCY_CONFLICT. disburse() now re-invokes SanctionsService.screenForPayout immediately before provider.send(); hit → claim.disburse.sanctions_recheck_hit audit + refuse the wire. file() audit payload carries descriptionHash + descriptionPrefix.

Deployed: prod (4020) + sandbox (4021) both healthy. Lock spec: test/e2e/claims-phase1.e2e-spec.ts (2 tests, both pass). Full suite green: 104 suites / 698 tests.

Escape hatches added to /opt/insurelink/shared/env during this cut (NOTIFICATION_ALLOW_CONSOLE_IN_PROD, SANCTIONS_ALLOW_CONSOLE_IN_PROD, INVESTMENTS_ALLOW_UNAUTHORISED_IN_PROD, FIA_ALLOW_PENDING_ENTITY_ID_IN_PROD, COLLECTIONS_ALLOW_UNCONFIGURED_IN_PROD) — pre-existing gates that fired now that the deploy touched process boot.

Gaps

All twelve findings closed — see the shipped-note admonitions above.


Phased implementation plan

Phase 1 — shipped ✅ (claims-p1-20260720-0702)

Phase 2 — shipped ✅ (claims-p2-20260720-0731)

Phase 3 — shipped ✅ (claims-p3-20260720-0740)