Skip to main content

ADR-0153: Persist the AMLR Art. 19 CDD-trigger record at case creation

Date: 2026-07-28 Status: Accepted Deciders: Adrian (Soft4U), Claude (Opus 4.8)

Context

The AMLR (Reg. (EU) 2024/1624) Art. 19 CDD-trigger record answers why customer due diligence applies to a customer, and under which risk-configuration version it was determined. Before this decision the record was recomputed at every read from the pure compute_cdd_trigger_record, using the current config and a read-time timestamp — so a later risk-config change (a tightened Art. 19 threshold, a widened customer scope) silently changed what the historical record claimed applied when the relationship was established, and successive reads of the same case produced different records. For an immutable, regulator-facing determination (EU AI Act Art. 12; AMLR 5-year retention) the record must be pinned at creation.

Forces:

  • The record must be immutable and reproducible — the same case reads the same record forever, pinned to the config version in force at creation.
  • Case creation must never break on a side-record failure (the case row is the primary artifact; the trigger record is secondary).
  • Several code paths create cases (create_case, bulk import, scan-escalation, periodic review), and only genuine new-relationship onboarding establishes an Art. 19(1)(a) trigger — a rerun/review/scan re-investigates an existing relationship.

Decision

Persist the Art. 19 CDD-trigger record at case creation, in two places with distinct roles:

  1. Immutable source of truth — a hash-chained cdd_trigger_record_created event in audit_events (ADR-0064/0109), written after insert_case (its case_id FK is ON DELETE RESTRICT, so the case row must exist first).
  2. Read cache — a copy on cases.additional_data["cdd_trigger_record"], inserted with the case row, which downstream reads prefer over recomputation.

The record pins the config version and the effective Art. 19 values in force under it (thresholds + statutory customer scope), and records all Art. 19(1) circumstances: (a) business_relationship in-scope; (b)/(2) transaction triggers declared out_of_scope (no transaction monitoring — MLRO scoping); (c) legal-entity creation, (d) suspicion of ML/TF, (e) doubts about previously-obtained data, (f) doubts about the interacting person recorded not_assessed (in scope, but investigation-time determinations not evaluable at creation). Clause lettering is verified verbatim against the Lex corpus.

Derived cases (rerun / periodic review / scan escalation) do not get a founding trigger — they re-investigate an existing relationship whose Art. 19(1)(a) trigger was recorded at original onboarding. A single shared is_derived_case predicate gates every write and read; the rerun marker is validated (verify_case_tenant) so caller metadata cannot forge a trigger-skip. A legacy onboarding case with no persisted record synthesizes the founding trigger deterministically from the case's stable created_at.

Failure handling is guard-and-swallow but loud: audit_cdd_trigger_record logs at ERROR (never silently) if the immutable write fails, so only the mutable cache remains until it is retried — an observable, retriable state, not a silent violation.

Consequences

Positive

  • The trigger record is immutable, reproducible, and pinned to the creation-time config — a later config change never rewrites history.
  • Every Art. 19(1) circumstance is honestly represented (never silently omitted); derived cases never falsely assert a new relationship.

Negative

  • Non-atomic recovery: the immutable audit write is not in the case-insert transaction (the FK forbids it), so a crash between insert and audit leaves only the mutable cache. Accepted for the PoC; a durable outbox / reconciliation path is a tracked follow-up (#838). The failure is loud (ERROR), not silent.
  • No investigation-time re-evaluation yet: the not_assessed (d)/(e)/(f)/(c) triggers are not updated when the investigation later establishes suspicion or identity-data doubts. Tracked as a follow-up (#838); the creation-time record honestly reads not_assessed, never a false clear.
  • Two representations (immutable event + mutable cache) must be kept consistent; reads prefer the cache, and the event is the audit anchor.

Neutral

  • Adds two CddTriggerBasis enum values ((c) legal_entity_creation, (f) doubt_about_interacting_person) and an art19_effective_config block to the payload.

Alternatives Considered

Alternative 1: Recompute the trigger at read time (the prior behavior)

  • No storage; always reflects current config.
  • Why rejected: non-reproducible (read-time timestamp) and unfaithful — a later config change rewrites what the historical determination claimed, defeating the immutable-record purpose.

Alternative 2: Persist only the immutable audit event (no additional_data cache)

  • Single source of truth, no dual-representation consistency concern.
  • Why rejected: every read surface (case detail, Wave-3 CDD endpoint, PDFs) would query and replay the audit chain; the cache on additional_data (inserted atomically with the case) is the low-latency read path, with the event as the tamper-evident anchor.

Alternative 3: Write the audit event inside the case-insert transaction (atomic)

  • Eliminates the non-atomic-recovery gap.
  • Why rejected: audit_events.case_id is an ON DELETE RESTRICT FK to cases.case_id; the case row must be committed before the event can reference it. A durable outbox (#838) is the correct atomicity fix, deferred.