Skip to content

SD03 — AML Monitoring data model

Database: bank_aml (Neon Postgres, ap-southeast-2) Repo: bank-aml ADRs: ADR-001 (Postgres as OLTP), ADR-024 (Neon hosting; partially superseded by ADR-052 — naming convention correction only; all other decisions remain in force), ADR-003 (CDC to Snowflake), ADR-035 (Snowflake data residency), ADR-048 (database-enforced invariants) Schema owner: SD03 AML Transaction Monitoring Platform

This is the authoritative schema reference for all modules in bank-aml. Use exact column names and types from this document — do not invent alternatives.

The aml schema is the primary write domain for MOD-016 (rule-based typology engine), MOD-017 (ML behavioural scoring), MOD-018 (alert case management), and MOD-019 (regulatory report submission). All alert and case records are immutable once created — status transitions are tracked by inserting new event rows, not updating the original record. Regulatory submissions, once filed, are never edited.


Schema: aml

aml.aml_alerts

One row per alert generated by MOD-016 (rule engine) or MOD-017 (ML model). Alerts may be dismissed or escalated into cases. An alert that does not result in a case still retains its record for audit.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
alert_type text NOT NULL CHECK (alert_type IN ('RULE','ML_MODEL','COMBINED')) Generating source
typology_code text NOT NULL Typology identifier (e.g. STRUCT_001, RAPID_MOV_003)
rule_version text Rule set version; NULL for ML-only alerts
model_version text ML model version; NULL for rule-only alerts
risk_score numeric(5,2) CHECK (risk_score IS NULL OR (risk_score >= 0 AND risk_score <= 100)) Composite alert risk score 0–100
alert_status text NOT NULL CHECK (alert_status IN ('OPEN','UNDER_REVIEW','DISMISSED','ESCALATED_TO_CASE','CLOSED'))
triggered_at timestamptz NOT NULL DEFAULT now()
reviewed_at timestamptz When analyst first opened the alert
closed_at timestamptz
assigned_to text Staff ID of assigned analyst
trigger_transactions jsonb NOT NULL DEFAULT '[]' Array of payment_ids that triggered the alert
trigger_window_start timestamptz Start of the transaction window assessed
trigger_window_end timestamptz End of the transaction window assessed
case_id uuid REFERENCES aml.aml_cases(id) DEFERRABLE INITIALLY DEFERRED Populated when alert is escalated; FK retro-added by MOD-018 V001 (was a plain uuid in MOD-016 V001 because aml_cases did not yet exist)
policy_refs jsonb NOT NULL DEFAULT '["AML-005"]'
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL

Indexes: - idx_aml_alerts_party_id on (party_id) - idx_aml_alerts_status on (alert_status) WHERE alert_status IN ('OPEN','UNDER_REVIEW') - idx_aml_alerts_triggered_at on (triggered_at DESC) - idx_aml_alerts_typology_code on (typology_code)

Notes: trigger_transactions stores payment IDs for traceability but is not a FK-enforced array. Payment records may exist in SD04 at the time of alert creation. Analysts use the case management UI (MOD-018) to navigate from alert to payment details.


aml.aml_cases

A case groups one or more related alerts for consolidated analyst review. Cases may result in a Suspicious Activity Report (SAR) / Suspicious Matter Report (SMR). Case creation is initiated by MOD-018 when an analyst escalates an alert.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
case_reference text NOT NULL UNIQUE Human-readable case reference (e.g. CASE-2026-004821)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
case_type text NOT NULL CHECK (case_type IN ('SUSPICIOUS_ACTIVITY','STRUCTURING','SANCTIONS_BREACH','PEP_REVIEW','FRAUD_AML','OTHER'))
case_status text NOT NULL CHECK (case_status IN ('OPEN','UNDER_REVIEW','PENDING_SAR','SAR_FILED','CLOSED_NO_ACTION','CLOSED_REFERRED'))
risk_level text NOT NULL CHECK (risk_level IN ('LOW','MEDIUM','HIGH','CRITICAL'))
opened_at timestamptz NOT NULL DEFAULT now()
closed_at timestamptz
assigned_to text Primary analyst staff ID
supervisor_id text Supervisor staff ID for CRITICAL cases
narrative text Case summary narrative; required before closure
sar_required boolean NOT NULL DEFAULT false Whether case will result in a SAR/SMR filing
submission_id uuid REFERENCES aml.regulatory_submissions(id) Populated after SAR is filed
thirty_day_deadline date Statutory SAR filing deadline
max_alert_risk_score numeric(5,2) NOT NULL DEFAULT 0 Highest risk_score across alerts attached to this case; updated by MOD-018 whenever an alert is attached. Drives the SAR threshold gate (FR-116)
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')) Jurisdiction of the case — required for jurisdictional reporting and event payloads
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL

Indexes: - idx_aml_cases_party_id on (party_id) - idx_aml_cases_status on (case_status) WHERE case_status NOT IN ('CLOSED_NO_ACTION','CLOSED_REFERRED') - idx_aml_cases_sar_deadline on (thirty_day_deadline) WHERE sar_required = true AND case_status = 'PENDING_SAR' - idx_aml_cases_case_reference on (case_reference) - idx_aml_cases_jurisdiction on (jurisdiction)


aml.case_events

Append-only audit ledger for every action taken on a case, written by MOD-018. One row per event — case opened, alert attached, status changed, note added, escalation, supervisor approval, closure. Required for FR-115 evidence; satisfies AML-005, AML-006, and GOV-006 in LOG mode.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
case_id uuid NOT NULL REFERENCES aml.aml_cases(id)
event_type text NOT NULL CHECK (event_type IN ('CASE_OPENED','ALERT_ATTACHED','CASE_ASSIGNED','CASE_ACCEPTED','CASE_DECLINED','CASE_REASSIGNED','NOTE_ADDED','STATUS_CHANGED','CASE_ESCALATED','CASE_SUPERVISOR_APPROVED','CASE_CLOSED'))
occurred_at timestamptz NOT NULL DEFAULT now()
actor_staff_id text NULL for SYSTEM-generated events
actor_kind text NOT NULL CHECK (actor_kind IN ('staff','system'))
detail jsonb NOT NULL DEFAULT '{}' Structured event payload; PII must be redacted before insert
trace_id uuid NOT NULL Correlation ID for distributed tracing
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_case_events_case_id_occurred_at on (case_id, occurred_at DESC) - idx_case_events_event_type on (event_type) - idx_case_events_actor_staff_id on (actor_staff_id) WHERE actor_staff_id IS NOT NULL

Notes: Append-only — enforced by BEFORE-row trigger (re-uses MOD-016's aml.reject_mutation() function, ADR-048 Category 1). No updated_at column.


aml.analyst_pool

Ops-managed registry of AML compliance analysts and supervisors. The is_supervisor flag drives the FR-116 NO_ACTION close gate: only a supervisor may approve a case closure with no SAR filed when max_alert_risk_score exceeds the AppConfig threshold. Managed via the MOD-018 internal admin API.

Column Type Constraints Description
staff_id text PK Staff identifier (matches SSO/Cognito username)
display_name text NOT NULL
email text NOT NULL
is_supervisor boolean NOT NULL DEFAULT false Whether this analyst can approve NO_ACTION closures above the SAR threshold
active boolean NOT NULL DEFAULT true Inactive analysts are removed from the round-robin queue
last_assigned_at timestamptz Updated on each round-robin assignment; NULLS FIRST in ordering
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_analyst_pool_active on (active) WHERE active = true - idx_analyst_pool_supervisors on (is_supervisor) WHERE is_supervisor = true AND active = true - idx_analyst_pool_round_robin on (last_assigned_at NULLS FIRST) WHERE active = true AND is_supervisor = false


aml.case_assignments

Per-attempt assignment record tracking every offer, acceptance, decline, and supersession in the case workload balancer. Prevents the round-robin balancer from re-offering a declined case to the same analyst. One row per assignment attempt; a case may have multiple rows (e.g. declined then re-assigned).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
case_id uuid NOT NULL REFERENCES aml.aml_cases(id)
staff_id text NOT NULL REFERENCES aml.analyst_pool(staff_id)
assigned_at timestamptz NOT NULL DEFAULT now()
accepted_at timestamptz NULL until analyst accepts
declined_at timestamptz NULL unless analyst declines
decline_reason text
superseded_at timestamptz Set when a later assignment supersedes this one
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now()

CHECK (accepted_at IS NULL OR declined_at IS NULL) — a single assignment attempt cannot be both accepted and declined.

Indexes: - idx_case_assignments_case_id on (case_id) - idx_case_assignments_staff_active on (staff_id) WHERE accepted_at IS NULL AND declined_at IS NULL AND superseded_at IS NULL


aml.typology_matches

Detailed record of each rule or model pattern match that contributed to an alert. One alert can have multiple typology matches if multiple rules fired. Used for pattern analysis and rule refinement in Snowflake.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
alert_id uuid NOT NULL REFERENCES aml.aml_alerts(id)
typology_code text NOT NULL Specific typology pattern code
typology_description text NOT NULL Human-readable typology description
match_type text NOT NULL CHECK (match_type IN ('RULE','ML_FEATURE','GRAPH_PATTERN'))
rule_id text Rule identifier if match_type = RULE
feature_name text ML feature if match_type = ML_FEATURE
observed_value numeric(18,4) Numeric value that triggered the match
threshold_value numeric(18,4) Threshold the observed value crossed
confidence numeric(5,2) CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 100)) Match confidence 0–100 for ML matches
transaction_ids jsonb NOT NULL DEFAULT '[]' Specific transactions involved in this match
matched_at timestamptz NOT NULL DEFAULT now()
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_typology_matches_alert_id on (alert_id) - idx_typology_matches_typology_code on (typology_code) - idx_typology_matches_matched_at on (matched_at DESC)

Notes: Rows are immutable after insertion — enforced by trg_typology_matches_immutable (ADR-048 Category 1). Pattern match evidence must not be altered after an alert is created.


aml.regulatory_submissions

Immutable record of every regulatory report filed — IFTI (AU, AUSTRAC), CMIR (NZ, RBNZ), SAR/SMR. Submissions are never edited after filing. Amendments create a new row with amends_submission_id set.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
submission_reference text NOT NULL UNIQUE Bank's internal reference for the submission
report_type text NOT NULL CHECK (report_type IN ('IFTI','CMIR','SAR','SMR','TTR','SUSPICIOUS_MATTER'))
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
regulator text NOT NULL e.g. AUSTRAC, RBNZ
subject_party_id uuid cross-domain ref to SD02 party.parties(party_id) — NULL for bulk threshold reports
related_case_id uuid REFERENCES aml.aml_cases(id) For SAR/SMR submissions
payment_ids jsonb NOT NULL DEFAULT '[]' Payment IDs included in this report
submitted_at timestamptz NULL until confirmed submitted
submission_status text NOT NULL CHECK (submission_status IN ('DRAFT','PENDING_REVIEW','APPROVED','SUBMITTED','ACKNOWLEDGED','REJECTED'))
regulator_reference text Reference number returned by the regulator
regulator_acknowledged_at timestamptz When regulator confirmed receipt
prepared_by text NOT NULL Module or staff ID that prepared the report
approved_by text Staff ID of approver (required before submission)
report_payload jsonb NOT NULL DEFAULT '{}' Full structured report payload as submitted
content_hash text NOT NULL SHA-256 of report_payload serialised as canonical UTF-8 JSON, computed at INSERT time — FR-119 tamper-evidence requirement
amends_submission_id uuid REFERENCES aml.regulatory_submissions(id) For amendment submissions
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_regulatory_submissions_report_type_jurisdiction on (report_type, jurisdiction) - idx_regulatory_submissions_submitted_at on (submitted_at DESC) WHERE submitted_at IS NOT NULL - idx_regulatory_submissions_party_id on (subject_party_id) WHERE subject_party_id IS NOT NULL - idx_regulatory_submissions_status on (submission_status) WHERE submission_status IN ('DRAFT','PENDING_REVIEW','APPROVED')

Notes: This table captures the filed artifact. MOD-019 is responsible for generating the structured report_payload in the regulator-specific format (AUSTRAC GoAML schema for IFTI; RBNZ submission schema for CMIR). The payload is stored here for evidence of what was submitted, not for re-processing. Immutability is enforced by trg_regulatory_submissions_immutable (ADR-048 Category 1).


aml.posting_history_cache

Local materialised cache of bank.core.posting_completed EventBridge events. Written by the MOD-016 ingest Lambda on every posting event received from the bank-core bus. Provides the lookback transaction history that pattern rules (structuring, rapid-movement, round-trip) need for multi-transaction window analysis. This is the authoritative hot-path data source for MOD-016 rule evaluation — Snowflake/Iceberg is not on the real-time path.

Rows are insert-once and never updated. A scheduled Lambda prunes rows older than the configurable retention window (default 90 days, stored in AppConfig). payment_id has a UNIQUE constraint to prevent duplicate entries from event replay.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
party_id uuid NOT NULL Cross-domain ref to SD02 party.parties(party_id)
account_id uuid NOT NULL Account posted to in SD01
payment_id uuid NOT NULL UNIQUE From SD04 payments — dedup key on ingest
direction text NOT NULL CHECK (direction IN ('DEBIT','CREDIT'))
amount_nzd numeric(18,4) NOT NULL Amount normalised to NZD for cross-currency pattern rules
original_amount numeric(18,4) NOT NULL Original transaction amount
original_currency char(3) NOT NULL ISO 4217 currency code
counterparty_account_id uuid SD01 account ID for internal transfers; NULL for external
counterparty_bsb text AU BSB or NZ bank number for external counterparties
channel text SWIFT / OSKO / RTGS / INTERNAL / DIRECT_DEBIT
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')) Jurisdiction from the source posting event — required for jurisdiction-aware typology rules (HIRISK_GEO_001, CASH_THR_001)
posted_at timestamptz NOT NULL When posting was committed in SD01
cached_at timestamptz NOT NULL DEFAULT now() When this cache row was written

Indexes: - idx_phc_party_posted_at on (party_id, posted_at DESC) — primary lookback query pattern - idx_phc_payment_id on (payment_id) — dedup check on ingest - idx_phc_posted_at on (posted_at DESC) — retention pruning sweep


aml.rule_definitions

Registry of all configured typology rules. Each rule has a stable rule_id, a typology code, and a JSONB default parameters blob. Live rule parameters are stored in AppConfig (FR-107, ADR-033) for sub-5-minute hot reload; this table records the rule's identity, description, and the AppConfig profile key where live parameters live.

This table is the source-of-truth for which rules are registered. AppConfig is the source-of-truth for current live parameter values.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
rule_id text NOT NULL UNIQUE Stable machine identifier (e.g. STRUCT_001, RAPID_MOV_003)
typology_code text NOT NULL AML typology code; one typology may have multiple rules
rule_name text NOT NULL Human-readable rule name
description text NOT NULL What pattern this rule detects
enabled boolean NOT NULL DEFAULT true Soft-disable without deleting the rule definition
default_parameters jsonb NOT NULL DEFAULT '{}' Default thresholds and window sizes — not the live values
appconfig_profile_key text NOT NULL Key in AppConfig where live parameters are stored
created_at timestamptz NOT NULL DEFAULT now()
created_by text NOT NULL Staff ID or module ID that registered the rule
retired_at timestamptz Set when rule is permanently retired

Indexes: - idx_rule_definitions_typology_code on (typology_code) - idx_rule_definitions_enabled on (enabled) WHERE enabled = true


aml.rule_executions

Append-only audit log of every rule evaluation run. Records whether each rule fired, passed, or errored for a given transaction. Required for FR-108 — the audit trail must cover non-alert outcomes (passes and errors), not only alerts. High-volume table: approximately one row per rule per transaction evaluated. Rows are immutable.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
payment_id uuid NOT NULL Payment that triggered evaluation
party_id uuid NOT NULL Party whose history was evaluated
rule_id text NOT NULL REFERENCES aml.rule_definitions(rule_id)
rule_version text NOT NULL AppConfig deployment version at time of evaluation
evaluated_at timestamptz NOT NULL DEFAULT now()
window_start timestamptz Start of transaction history window assessed (if applicable)
window_end timestamptz End of transaction history window assessed
result text NOT NULL CHECK (result IN ('ALERT','PASS','ERROR'))
alert_id uuid REFERENCES aml.aml_alerts(id) Populated when result = ALERT
execution_ms integer Rule evaluation duration for this row
error_detail text Error message when result = ERROR
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_rule_executions_payment_id on (payment_id) - idx_rule_executions_party_evaluated_at on (party_id, evaluated_at DESC) - idx_rule_executions_rule_id_evaluated_at on (rule_id, evaluated_at DESC) - idx_rule_executions_result on (result) WHERE result IN ('ALERT','ERROR')

Notes: If row volume exceeds 50M/month, partition by evaluated_at month. Rows are never updated or deleted within the regulatory retention period. Immutability is enforced by trg_rule_executions_immutable (ADR-048 Category 1).


aml.rule_config_history

Bank-internal audit log of every change to rule parameters. Every time live parameters change in AppConfig, MOD-016 writes an immutable row capturing the previous and new values, who made the change, and a mandatory justification. AppConfig's own version history is the primary operational log; this table provides a bank-native queryable audit trail for regulatory examination.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
rule_id text NOT NULL REFERENCES aml.rule_definitions(rule_id)
changed_at timestamptz NOT NULL DEFAULT now()
changed_by text NOT NULL Staff ID of person who deployed the config change
previous_config jsonb NOT NULL Full parameter values before the change
new_config jsonb NOT NULL Full parameter values after the change
appconfig_version text NOT NULL AppConfig deployment version identifier
change_reason text NOT NULL Mandatory documented justification for the change
effective_from timestamptz NOT NULL When the new config became live in AppConfig
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_rule_config_history_rule_id_changed_at on (rule_id, changed_at DESC)

Notes: change_reason NOT NULL enforces documented justification for every rule parameter change. Rows are immutable — never updated or deleted. Immutability is enforced by trg_rule_config_history_immutable (ADR-048 Category 1).


aml.behavioural_scores

Pre-computed ML behavioural risk scores for each customer, written by MOD-017's write-back Lambda following the ADR-047 pattern (Snowflake Cortex → Neon). MOD-016 reads this table on every posting event to produce COMBINED or ML_MODEL alerts. The lookup replaces inline inference and satisfies NFR-021 (≤200ms) with a sub-10ms Postgres read.

Score freshness: the nightly Snowflake Task re-scores all customers active in the last 90 days. valid_until is derived by the write-back Lambda from the /bank/{stage}/mod-017/model/validity-hours SSM parameter (default 24h if unset) — the Snowflake Task does not include valid_until in the payload, so the freshness window can be retuned without redeploying Snowflake. In practice the Lambda sets valid_until = scored_at + validity_hours, yielding approximately 26 hours ahead under the default. Event-triggered incremental re-scores (on significant posting events) refresh within 15 minutes. MOD-016 checks valid_until before consuming the score — if stale it runs rule-only and records alert_type = 'RULE' rather than 'COMBINED'. Rows are immutable; score supersession inserts a new row. Old rows are retained for FR-111 retrospective model performance analysis. Champion/challenger governance (DT-005) is supported via model_role.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
party_id uuid NOT NULL Cross-domain ref to SD02 party.parties(party_id)
model_version text NOT NULL Cortex model version identifier (e.g. v1.2-champion)
model_role text NOT NULL CHECK (model_role IN ('CHAMPION','CHALLENGER')) Champion is the operative scoring model; challenger is evaluated alongside for performance comparison
score numeric(5,0) NOT NULL CHECK (score >= 0 AND score <= 1000) Behavioural risk score 0–1000 (FR-109)
risk_tier text NOT NULL CHECK (risk_tier IN ('LOW','MEDIUM','HIGH','CRITICAL')) Derived tier — aligns with AML alert risk levels
feature_vector_hash text NOT NULL SHA256 of the feature vector used; enables retrospective model analysis without reprocessing raw data (FR-111)
score_reasons jsonb NOT NULL DEFAULT '[]' Top contributing features and their weights
scored_at timestamptz NOT NULL When Snowflake Cortex ran the inference
valid_until timestamptz NOT NULL Freshness boundary — MOD-016 treats scores past this timestamp as stale and falls back to rule-only
triggered_by text NOT NULL CHECK (triggered_by IN ('SCHEDULED','EVENT')) Whether produced by the nightly batch or an event-triggered incremental run
source_event_id text Posting event ID that triggered an EVENT re-score; NULL for SCHEDULED runs
created_at timestamptz NOT NULL DEFAULT now()

UNIQUE (party_id, model_version, scored_at) — retry-safe idempotency key. The write-back Lambda uses INSERT ... ON CONFLICT (party_id, model_version, scored_at) DO NOTHING. Multiple scored_at rows for the same party and model version are legitimate (successive re-scores retained for FR-111 retrospective analysis).

Indexes: - idx_behavioural_scores_party_champion on (party_id, scored_at DESC) WHERE model_role = 'CHAMPION' — primary MOD-016 read pattern - idx_behavioural_scores_valid_until_champion on (valid_until) WHERE model_role = 'CHAMPION' — freshness sweeper - idx_behavioural_scores_model_version on (model_version, scored_at DESC) — champion/challenger management and rollback

Notes: Rows are immutable after insertion — enforced by trg_behavioural_scores_immutable (ADR-048 Category 1). Write-back follows ADR-047: MOD-017 owns the Snowflake Task and write-back Lambda that delivers scores from risk_aml.behavioural_model_scores in SD06 Snowflake to this table. FR-112 rollback: reconfigure the Snowflake Task to the previous model version and trigger one full write-back run — achievable within 30 minutes.


aml.risk_scores_mirror

Local mirror of MOD-039's customer composite risk scores, populated by the MOD-016 risk-score-consumer Lambda from the bank.risk-platform / customer_risk_score_updated EventBridge event. Read by AML typology rules (MOD-016) and ML features (MOD-017) — avoids cross-bus or Snowflake calls on the hot path.

Column Type Constraint Notes
party_id uuid NOT NULL Cross-domain ref to SD02 party.parties.party_id — no FK (cross-database; application-enforced)
composite_risk_score double precision NOT NULL MOD-039 XGBoost composite score, 0–100
risk_tier varchar(10) NOT NULL CHECK (LOW|MEDIUM|HIGH|CRITICAL)
previous_risk_tier varchar(10) nullable, CHECK same enum Null on first-ever score for the party
tier_changed boolean NOT NULL
score_version varchar(50) NOT NULL Producer's model version label
scored_at timestamptz NOT NULL From event detail (not envelope event_time)
idempotency_key varchar(200) NOT NULL From event detail; trace-correlation
mirror_updated_at timestamptz NOT NULL DEFAULT now() Local write timestamp
PRIMARY KEY (party_id, scored_at) Natural dedup key

Indexes: - idx_risk_scores_mirror_party_id_scored_at on (party_id, scored_at DESC) — primary read pattern (getLatestRiskScore)

Invariants (ADR-048): - trg_risk_scores_mirror_immutable — append-only via aml.fn_immutable_row() SECURITY DEFINER - ON CONFLICT (party_id, scored_at) DO NOTHING at write time — EventBridge replays are idempotent

Grants: bank_aml_app_user: SELECT, INSERT — bank_aml_readonly: SELECT

Producer: SD06 / MOD-039 on the bank-risk-platform bus.


aml.model_deployment_history

Append-only audit log of all model lifecycle events — promotions to champion, rollbacks, challenger deployments, and retirements. Written by MOD-017. Required for DT-005 LOG mode (champion/challenger governance). One row per lifecycle event; rows are never updated or deleted.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
model_version text NOT NULL Model version being acted upon (e.g. v1.2)
previous_model_version text Preceding champion version; required on PROMOTED_TO_CHAMPION and ROLLED_BACK rows (enforced by chk_mdh_previous_version_required)
model_role text NOT NULL CHECK (model_role IN ('CHAMPION','CHALLENGER')) The model's role at the time of the event. Orthogonal to event_type: a ROLLED_BACK row may carry CHAMPION or CHALLENGER depending on which version was being rolled back. Required for DT-005 audit queries.
trace_id uuid NOT NULL Per ADR-031, every audit-class row carries a trace_id propagated from the originating runbook invocation
event_type text NOT NULL CHECK (event_type IN ('PROMOTED_TO_CHAMPION','ROLLED_BACK','RETIRED','CHALLENGER_DEPLOYED'))
effective_at timestamptz NOT NULL When the model lifecycle change took effect
deployed_by text NOT NULL Staff ID or automation principal that triggered the change
change_reason text NOT NULL Required human-readable rationale
champion_metrics jsonb Precision, recall, and AUC. Required on PROMOTED_TO_CHAMPION rows; must be NULL on all other event types (enforced by chk_mdh_promotion_carries_metrics)
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_model_deployment_history_effective_at on (effective_at DESC) - idx_model_deployment_history_model_version on (model_version) - idx_model_deployment_history_model_version_effective_at on (model_version, effective_at DESC) — ordered deployment timeline per model version - idx_model_deployment_history_event_type_effective_at on (event_type, effective_at DESC) — audit dashboard filter by event type - idx_model_deployment_history_rolled_back_effective_at on (effective_at DESC) WHERE event_type = 'ROLLED_BACK' — FR-112 hot path for most-recent rollback query

Notes: Rows are immutable after insertion — enforced by trg_model_deployment_history_immutable (ADR-048 Category 1). No updated_at column. The effective_at / created_at split supports audit queries that need to distinguish when the system recorded the event vs. when it took effect.


DB-enforced invariants (ADR-048)

Registers the Postgres constraints and triggers applied to this domain. All constraints and triggers are implemented in numbered Flyway migrations in bank-aml.

Immutability triggers

Table Trigger Category
aml.rule_executions trg_rule_executions_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.rule_config_history trg_rule_config_history_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.typology_matches trg_typology_matches_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.regulatory_submissions trg_regulatory_submissions_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.case_events BEFORE UPDATE OR DELETE, calls aml.reject_mutation() (MOD-016's shared immutability function — same semantics as fn_immutable_row()) Cat 1
aml.behavioural_scores trg_behavioural_scores_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.model_deployment_history trg_model_deployment_history_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
aml.risk_scores_mirror trg_risk_scores_mirror_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1

Exception — aml.posting_history_cache: Not covered by an immutability trigger. The scheduled retention sweeper intentionally hard-deletes rows older than the AppConfig-configured retention window (default 90 days). Application code enforces insert-only semantics on the write path; the sweeper is the only permitted deletion path.

CHECK constraints

Table Column Constraint
aml.aml_alerts risk_score CHECK (risk_score IS NULL OR (risk_score >= 0 AND risk_score <= 100))
aml.typology_matches confidence CHECK (confidence IS NULL OR (confidence >= 0 AND confidence <= 100))
aml.model_deployment_history chk_mdh_previous_version_required (event_type IN ('ROLLED_BACK','PROMOTED_TO_CHAMPION') AND previous_model_version IS NOT NULL) OR event_type IN ('CHALLENGER_DEPLOYED','RETIRED') — Cat 1: promotions and rollbacks must name the version they replace
aml.model_deployment_history chk_mdh_created_at_after_effective created_at >= effective_at - interval '1 minute' — Cat 1: audit row cannot be inserted significantly before the event it records; 1-minute clock-skew allowance
aml.model_deployment_history chk_mdh_promotion_carries_metrics event_type != 'PROMOTED_TO_CHAMPION' OR champion_metrics IS NOT NULL — Cat 2: champion promotions must carry real metrics; NULL on non-promotion rows

Not DB-enforced (Category 3 — cross-service or config-driven)

Rule Reason Owner
SAR threshold gate Threshold (default 70) is AppConfig-configurable — same inputs, different outcome at different config values MOD-018 Lambda
Escalation timer Depends on wall-clock and aml_cases.created_at; triggers supervisor escalation after configurable inactivity window MOD-018 Lambda
ML confidence gate Model version and confidence floor are config-driven MOD-017 Lambda

Cross-domain references

This table Column References Notes
aml.aml_alerts party_id SD02 party.parties(party_id) Alerts are always linked to a known party
aml.aml_cases party_id SD02 party.parties(party_id) Cases are anchored to a party record
aml.regulatory_submissions subject_party_id SD02 party.parties(party_id) SAR subjects must be registered parties
aml.aml_alerts trigger_transactions SD04 payments.payments(id) Stored in JSONB array (not FK-enforced); payments may precede alert creation
aml.posting_history_cache party_id SD02 party.parties(party_id) Denormalised from posting_completed event for query performance
aml.posting_history_cache account_id SD01 accounts.accounts(id) Denormalised from posting_completed event
aml.posting_history_cache payment_id SD04 payments.payments(id) Stored as reference; not FK-enforced (cross-database)
aml.rule_executions rule_id aml.rule_definitions(rule_id) FK-enforced; rule must exist before executions are logged
aml.rule_config_history rule_id aml.rule_definitions(rule_id) FK-enforced; rule must exist before config history can be written
aml.case_events case_id aml.aml_cases(id) FK-enforced; every event must belong to an existing case
aml.case_assignments case_id aml.aml_cases(id) FK-enforced
aml.case_assignments staff_id aml.analyst_pool(staff_id) FK-enforced; assignment target must be a registered analyst
aml.behavioural_scores party_id SD02 party.parties(party_id) Write-back target; every score row belongs to a registered party
aml.risk_scores_mirror party_id SD02 party.parties(party_id) Cross-domain ref; no FK (cross-database)

Consumers of SD03 data: SD06 Snowflake receives all aml schema tables via CDC (MOD-042) for the AUSTRAC/RBNZ AML reporting pipeline (MOD-037) and AML typology dashboard. SD08 back-office (MOD-074) reads aml_cases for customer 360 views visible to compliance staff. The regulatory_submissions table provides evidence for prudential and AML regulatory examinations.