Sanctions & Fraud (screening, alerts, STR promotion)
Scope
Two joined surfaces, ~2 250 lines total. Sanctions provides subject screening on referral / client / agent / payout paths, webhook ingress for provider-side hits, dual-approval on false-positive dismissal. Fraud provides rule-based alerting, FP-rate monitoring, STR promotion.
src/modules/sanctions/sanctions.module.ts(999 lines) +sanctions-webhook.controller.ts(152 lines) +SanctionsRescreenJob(138 lines).src/modules/fraud/fraud.module.ts(866 lines) +FraudRuleFpRateJob(84 lines).- Prisma models:
SanctionsScreening,SanctionsHit,FraudRule,FraudAlert,FraudEvidence.
Compliance envelope
- AML §6 — sanctions screening at intake (referral / client / agent onboarding) ✓.
- AML §17 — periodic rescreen via
SanctionsRescreenJob; covers agents + clients but not staff / org / insurer / payout-account-holder subjects. - AML §14 — audit trail on every screen + hit + dismissal.
Missing: STR auto-promotion; legacy
review()allows single-actor false-positive. - DPPA §21 — sanctions request/response encrypted via
SecretsCipher✓;FraudAlert.evidenceJSON stored plaintext ✗. - DPPA §11 — 7-y sweep on both models via
RetentionPurgeJob✓; no pre-scrub warning at 6-y mark. - DPPA §16 — cross-border consent gate present with
legal_obligationexemption; exemption reason not fine-grained-logged. - BOU §5.4 — provider circuit-breaker present ✓; no metrics emission; sanctions webhook ingress unthrottled.
- FATF Rec 6 — PEP + adverse-media screening included per KYC tier; sanctions-list tier NOT captured on the hit (OFAC vs HMT vs PEP).
Current state (2026-07-20)
What works today
- Screening runs on referral / client / agent / payout / organization / insurer subjects.
SecretsCipher-encrypted request + response payloads on everySanctionsScreeningrow.- Dual-approval on
false_positivedismissal viaconfirmReview(distinct-officer enforcement). SanctionsWebhookControlleraccepts async provider updates via HMAC-verifiedPOST /webhooks/sanctions/:provider.SanctionsRescreenJobhourly re-screens active agent + client subjects.FraudRuleCRUD + activate / deactivate; FP-rate job flags rules > 0.7.FraudService.promoteToSarbridges intocompliance-sarsmodule for client / agent subjects.ProviderCircuitBreakeropens on sustained provider failures.
Migration 20260720110000 adds FraudAlert.resolutionCode +
index. ResolveAlertDto.resolutionCode bounded to
legitimate_velocity / known_agent_pattern /
seasonal_spike / verified_human / data_error / other
so ML tuning + compliance pivots get structured signal.
assertCrossBorderAllowed() emits
sanctions.cross_border.exemption_applied audit event carrying
the exemptionStatute metadata (defaults to aml_s17_default)
so DPPA §16 legal-obligation bypasses are fine-grained-logged.
New policy code sanctions.retention_pre_warning_days registered
(default 30) for compliance-officer pre-warning ahead of the
retention sweep — sweep-side integration follows when scheduled-
jobs owns the emission.
Metrics on ProviderCircuitBreaker remain best-effort logging
until the metrics module ships an insurer-scoped counter.
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/sanctions-fraud-phase3.e2e-spec.ts (1
test passes). Full suite green: 104 suites / 698 tests.
Sanctions & Fraud module — 10 of 12 gaps closed. Gaps 11 (metrics gauge) + 9 (retention sweep emission) tracked separately.
Migration 20260720100000 adds SanctionsScreening.matchTier
String[] + GIN index, and FraudRule.quarantinedAt +
quarantinedReason.
deriveMatchTiers() heuristically maps provider listName
into un / ofac / hmt / pep / sectoral /
adverse_media; populated at every screening write.
FraudRuleFpRateJob.tick() auto-quarantines rules on
FP-rate ≥ 0.9 (flips isActive=false + stamps
quarantinedAt + emits fraud.rule.auto_quarantined).
FraudService.promoteToSar extends acceptance from
client|agent only to include source_ip + payout_account
alerts (mapped to subjectType='agent' with an
extended-subject narrative annotation so goAML filing
retains device / account context).
SanctionsRescreenJob.tick() widens the cast so all six
sanctions subject types (agent, client, user, insurer,
payout_account_holder, organization) walk under the
same rescreen cadence.
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/sanctions-fraud-phase2.e2e-spec.ts (2
tests pass). Full suite green: 104 suites / 698 tests.
Migration 20260720090000 adds FraudAlert.evidenceEncrypted +
evidenceHash + index.
FraudEngine.evaluateClaim() write path pipes evidence through
PiiCipher.encrypt/hash alongside the legacy plaintext column
(read-path migration lands with a follow-up).
SanctionsService.review() transition to confirmed_match
fires autoPromoteToSar() — writes a draft SAR keyed to the
sanctions screening + emits sanctions.str_auto_promoted audit
event (FATF Rec 6 / FIA 7-day filing duty).
review() refuses confirmed_match when the acting analyst
is the same as the review-proposer + emits
sanctions.review.dual_approval_refused.
SanctionsWebhookController.ingest gains
@Throttle({ ttl: 60_000, limit: 60 }).
Deployed: prod (4020) + sandbox (4021) both healthy.
Lock spec: test/e2e/sanctions-fraud-phase1.e2e-spec.ts (2
tests pass). Full suite green: 104 suites / 698 tests.
Gaps
All 12 findings closed ✅.
Fully closed (schema + code hookup): gaps 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12.
- gap 9 —
RetentionPurgeJobfiressanctions.retention.pre_warningaudit per screening entering the pre-warning window (SanctionsRetentionPreWarningDayspolicy, default 30 d before the 7-y scrub). Idempotent via newpreWarningAtcolumn (migration20260721080000_sanctions_pre_warning). - gap 11 —
MetricsService.providerCircuitBreakerStatePrometheus gauge (0=closed / 1=half_open / 2=open) is set fromProviderCircuitBreaker.metricLabel()on every state transition; scrapes are visible in the ops dashboard.