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:
- Immutable source of truth — a hash-chained
cdd_trigger_record_createdevent inaudit_events(ADR-0064/0109), written afterinsert_case(itscase_idFK isON DELETE RESTRICT, so the case row must exist first). - 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 readsnot_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
CddTriggerBasisenum values ((c) legal_entity_creation, (f) doubt_about_interacting_person) and anart19_effective_configblock 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_idis anON DELETE RESTRICTFK tocases.case_id; the case row must be committed before the event can reference it. A durable outbox (#838) is the correct atomicity fix, deferred.