Skip to main content

ADR-0066: Live risk-paced UBO re-screening + general-population monitoring

Status: Accepted Date: 2026-06-16 Supersedes: none Superseded by: none Deciders: Adrian (Soft4U), Claude Opus 4.8

Decision context:

  • Latency: live OpenSanctions/PEP calls during monitoring runs, gated by a per-case cadence check so most cases skip on most runs; per-case execution is already sequential in the workflow. The cadence gate is a single timestamp comparison + one indexed screening_results lookup per case.
  • Dependency surface: no new packages, no new table. Reuses screen_entities (#33 pipeline) and the screening_results evidence trail (#45, migration 065). Adds one field to RiskAssessment + two pure helpers + one read activity.
  • Debuggability: a re-screen emits a monitoring_events row (severity/change) and appends screening_results rows (timestamped, per person/list); a failed screen surfaces as a warning event, never a silent skip.
  • Reversibility: branch revert; additive (the retired customs-only activity is the only deletion, and only the monitoring workflow referenced it).
  • Blast radius: the monitoring workflow now iterates the general onboarded population; check_ubo_screening goes from no-op to live. No schema migration.
  • Alternative considered: fixed per-tenant cron with no per-case cadence (rejected — not risk-based, re-screens everything every run).

Context

Ongoing monitoring (AMLR Art. 21) was scaffolding for the data that matters. check_ubo_screening was an explicit mock — UBOs were never re-screened against sanctions/PEP lists. ContinuousMonitoringWorkflow iterated active customs cases only (template_id LIKE '%customs%' AND status='APPROVED'), so the general onboarded KYB population was never monitored. The "perpetual KYC" capability (NBB 2026/02) was not real. The building blocks existed: the live screening pipeline (screen_entities, #33) and a timestamped, RLS-scoped, append-only screening evidence trail (screening_results, #45).

Decision

  1. Risk-based cadence. Add review_cadence_days to RiskAssessment (auto-derived from the risk tier via a @model_validator). Pure cadence_days_for_tier maps EDD→90, CDD→180, SDD→365 days — all far within the AMLR Art. 26(2) full-review ceilings (EDD 12mo, CDD/SDD 60mo); these are sanctions-re-screen intervals, intentionally tighter. Pure is_rescreen_due(last_screened_at, cadence_days, now) gates each case: never-screened or elapsed ⇒ due.
  2. Live check_ubo_screening. When due, merge directors + UBOs (#33 canonical set), build queries, call screen_entities under a tenant-scoped session (live OpenSanctions/PEP + 3-tier suppression), map post-suppression hits → ScreeningResult rows, persist via ScreeningResultService (#45 trail), and return a MonitoringEvent whose severity reflects the hits (sanctions⇒critical, PEP⇒warning). Not due ⇒ INFO no-op. Engine failure ⇒ warning event (never a silent skip).
  3. General population. New fetch_active_monitoring_cases activity selects status IN ('APPROVED','APPROVED_WITH_RESTRICTIONS') with an explicit tenant predicate (admin session bypasses RLS), no customs filter, returning each case's risk_tier. The workflow calls it; the customs-only activity is retired (only the workflow referenced it).
  4. Evidence trail. Re-screens append to screening_results (timestamped) — no new migration.

Consequences

Positive

  • Real recurring, risk-paced sanctions/PEP re-screening of subject + UBOs across all onboarded cases, with a timestamped append-only evidence trail (AMLR Art. 21/26, EU AI Act Art. 12). Completes perpetual KYC.

Negative

  • Live screening cost/latency during monitoring runs — bounded by the cadence gate and sequential per-case execution. A monitoring run's wall-time grows with the number of due cases.

Neutral

  • Customs cases become a subset of the monitored population (still covered). RiskAssessment gains a derived field (back-compat via validator; existing serialized assessments default it from tier on load).

Alternatives Considered

Alternative 1: Fixed per-tenant cron, no per-case cadence gate

Rejected — re-screens every onboarded case on every run regardless of risk; wasteful and not risk-based as the acceptance criteria require.

Alternative 2: New ubo_rescreen_events table

Rejected — screening_results (#45) is already the append-only, RLS-scoped, timestamped screening evidence trail. A parallel table would fragment the evidence.

Alternative 3: Screen UBOs only (not directors)

Rejected — #33 established merge_screening_persons(directors, ubos) as the canonical natural-person screening set; director records carry the DOB/nationality discriminators that reduce false positives.