SD04 — Payments data model¶
Database: payments (Neon Postgres, ap-southeast-2)
Repo: bank-payments
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-048 (database-enforced invariants)
Schema owner: SD04 Payments Processing Platform
This is the authoritative schema reference for all modules in bank-payments. Use exact column names and types from this document — do not invent alternatives.
The payments schema is the primary write domain for MOD-020 (pre-payment validation), MOD-021 (payment limit and velocity controller), MOD-022 (payment audit trail), MOD-023 (transaction fraud scorer), MOD-024 (device and session intelligence), MOD-025 (FX rate lock and conversion), MOD-026 (IFTI/CMIR reporting trigger), MOD-061 (Open Banking API gateway), MOD-067 (trade finance), MOD-081 (payment reconciliation engine), MOD-082 (nostro and FX treasury management), MOD-084 (inbound CDR data retrieval), and MOD-120 (PayID registration and Osko real-time payments). All payment records are immutable once submitted. Payment events provide the full audit trail of state transitions.
Schema: payments¶
payments.payments¶
The primary payment instruction record. One row per payment instruction submitted, regardless of outcome. Created by MOD-020 during pre-payment validation, before the payment is sent to rails.
| 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) |
| from_account_id | uuid | NOT NULL REFERENCES accounts.accounts(id) | Debit account |
| to_account_id | uuid | REFERENCES accounts.accounts(id) | Credit account; NULL for outbound external payments |
| payment_type | text | NOT NULL CHECK (payment_type IN ('INTERNAL','NPP','RTGS','SWIFT','DIRECT_CREDIT','DIRECT_DEBIT','CARD','BPAY','OPEN_BANKING')) | |
| direction | text | NOT NULL CHECK (direction IN ('OUTBOUND','INBOUND','INTERNAL')) | |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | Instructed amount |
| currency | char(3) | NOT NULL | Instructed currency |
| settlement_amount | numeric(18,2) | Settled amount (may differ if FX conversion applied) | |
| settlement_currency | char(3) | ||
| fx_rate_lock_id | uuid | REFERENCES payments.fx_locks(id) | If FX conversion applied |
| status | text | NOT NULL CHECK (status IN ('SUBMITTED','VALIDATION_PENDING','VALIDATION_FAILED','PENDING_AUTH','AUTHORISED','PROCESSING','SETTLED','REVERSED','CANCELLED','RETURNED')) | |
| failure_reason | text | Populated when status = VALIDATION_FAILED or RETURNED | |
| payment_reference | text | Customer-supplied reference | |
| end_to_end_reference | text | ISO 20022 end-to-end identifier | |
| beneficiary_name | text | External payee name | |
| beneficiary_account | text | External account number (encrypted at rest) | |
| beneficiary_bank_code | text | BSB (AU) or bank code (NZ) | |
| beneficiary_country | char(2) | For cross-border payments | |
| fraud_score | numeric(5,2) | MOD-023 fraud score at time of submission | |
| sanctions_result_id | uuid | REFERENCES kyc.sanctions_results(id) | Sanctions screen result for this payment |
| initiated_at | timestamptz | NOT NULL DEFAULT now() | |
| settled_at | timestamptz | ||
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_payments_party_id on (party_id)
- idx_payments_from_account_id on (from_account_id)
- idx_payments_status on (status) WHERE status NOT IN ('SETTLED','CANCELLED','REVERSED')
- idx_payments_initiated_at on (initiated_at DESC)
- idx_payments_end_to_end_reference on (end_to_end_reference) WHERE end_to_end_reference IS NOT NULL
payments.payment_events¶
Append-only event log of every state transition for a payment. Provides the microsecond-timestamped audit trail required by MOD-022 and PAY-002 / REP-005. Never edited after insertion.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL REFERENCES payments.payments(id) | |
| event_type | text | NOT NULL CHECK (event_type IN ('SUBMITTED','VALIDATION_STARTED','BALANCE_CHECK','LIMIT_CHECK','SANCTIONS_CHECK','FRAUD_CHECK','ACCOUNT_STATUS_CHECK','VALIDATION_PASSED','VALIDATION_FAILED','AUTH_REQUESTED','AUTH_COMPLETED','ROUTING_STARTED','SENT_TO_RAIL','SETTLEMENT_CONFIRMED','REVERSAL_INITIATED','REVERSAL_CONFIRMED','CANCELLATION','RETURN_RECEIVED')) | |
| event_status | text | NOT NULL CHECK (event_status IN ('PASS','FAIL','PENDING','INFO')) | |
| event_timestamp | timestamptz | NOT NULL DEFAULT now() | Wall-clock time with microsecond precision |
| source_module | text | NOT NULL | Module that recorded this event |
| actor | text | Staff ID, module ID, or external system for the action | |
| channel | text | NOT NULL DEFAULT 'SYSTEM' CHECK (channel IN ('APP','API','OPEN_BANKING','AGENT','SYSTEM')) | FR-132 — channel on every record; SYSTEM for automated/rail events |
| ip_address | inet | FR-132 — operator-initiated payments only; NULL for automated/system events | |
| trace_id | uuid | ADR-031 trace propagation; NULL only if no trace context is available | |
| source_event_id | text | NOT NULL | Upstream EventBridge event_id — deduplicates EB redeliveries |
| details | jsonb | NOT NULL DEFAULT '{}' | Structured event-specific payload |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_payment_events_payment_id_timestamp on (payment_id, event_timestamp)
- idx_payment_events_event_type on (event_type)
- idx_payment_events_timestamp on (event_timestamp DESC)
- idx_payment_events_source_event_id UNIQUE on (source_event_id) — EventBridge redelivery deduplication
Notes: This table is append-only. No updated_at column. Every validation step in MOD-020 records at least one event row. The full payment lifecycle is reconstructable from this table alone. Immutability is enforced by trg_payment_events_immutable (ADR-048 Category 1). FR-132 fields (channel, ip_address) are real columns, not jsonb, to enable NOT NULL enforcement and indexed queries.
payments.intra_bank_transfers¶
Rail-specific transfer record for intra-institution payments. One row per transfer attempt. Owned by MOD-141. The audit chain is handled by MOD-020 (payment_events) + MOD-001 (posting_completed) + MOD-022 (SETTLEMENT_CONFIRMED) — MOD-141 emits no bus events directly.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL | Minted by MOD-141 at INSERT; passed to MOD-020 and MOD-001 for audit linkage. No FK constraint — payments.payments row is created by MOD-020 concurrently during the same flow |
| idempotency_key | text | NOT NULL | MOD-141 idempotency key; threads through MOD-020 and MOD-001 unchanged |
| source_account_id | uuid | NOT NULL | Debit leg account |
| destination_account_id | uuid | NOT NULL | Credit leg account; caller-supplied as resolved-internal (FR-625 routing detection deferred) |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL | ISO 4217 |
| channel | text | NOT NULL CHECK (channel IN ('APP','API','BACK_OFFICE','BATCH')) | Caller-supplied |
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','POSTED','FAILED')) | |
| posting_id | uuid | Set on POSTED; FK to SD01 accounts.postings(id) at application layer |
|
| failure_reason | text | Set on FAILED; e.g. VALIDATION_FAILED, INSUFFICIENT_FUNDS, STEP_UP_REQUIRED, POSTING_ERROR |
|
| narrative | text | Customer-supplied payment reference | |
| jurisdiction | char(2) | NOT NULL CHECK (jurisdiction IN ('NZ','AU')) | |
| trace_id | uuid | NOT NULL | ADR-031 |
| requested_at | timestamptz | NOT NULL | Caller-supplied request timestamp |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | Touch trigger maintains |
Indexes:
- UNIQUE (idempotency_key) — defence-in-depth replay guard (application-layer idempotency via payments.idempotency_keys is primary)
- idx_intra_bank_transfers_payment_id on (payment_id)
- idx_intra_bank_transfers_source_account_id on (source_account_id)
- idx_intra_bank_transfers_status on (status)
Notes: Uppercase status and channel enums are consistent with the rest of SD04. payment_id is NOT NULL — minted by MOD-141 before the row is inserted, so the value is always known at INSERT time. No FK to payments.payments because the payments row is created by MOD-020 in the same synchronous chain (inserting with a FK would require deferred constraint evaluation or nullable). posting_id is application-layer only for the same reason. STEP_UP_REQUIRED maps to status='FAILED' — the step-up retry loop is v2 (MOD-068 integration).
payments.bpay_payments¶
Rail-specific payment record for BPAY bill payments. One row per payment attempt. Owned by MOD-119. AU-only. The audit chain is handled by MOD-020 (payment_events) + MOD-001 (posting_completed) + MOD-022 (SETTLEMENT_CONFIRMED / REVERSAL_CONFIRMED).
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL | Minted by MOD-119 at INSERT; passed to MOD-020 and MOD-001 for audit linkage. No FK — payments.payments row created by MOD-020 in the same synchronous chain |
| idempotency_key | text | NOT NULL | MOD-119 idempotency key; threads through MOD-020 and MOD-001 unchanged |
| trace_id | uuid | NOT NULL | ADR-031 |
| from_account_id | uuid | NOT NULL | Debit account; application-layer only (no FK — cross-DB to SD01) |
| biller_code | text | NOT NULL | BPAY biller code; looked up in bpay_biller_cache |
| customer_reference | text | NOT NULL | Customer Reference Number (CRN); Luhn or regex validated before MOD-020 |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL DEFAULT 'AUD' | BPAY is AU-only |
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','SUBMITTING','SUBMITTED','SETTLED','RETURNED','DISPUTED','FAILED')) | SUBMITTING = between MOD-001 debit commit and sponsor HTTP call |
| posting_id | uuid | Set on SUBMITTING; debit posting reference. Application-layer only (no FK) | |
| reversal_posting_id | uuid | Set on RETURNED; credit-back posting reference. Application-layer only | |
| sponsor_reference | text | Set on SUBMITTED; sponsor bank's reference for the submission | |
| batch_id | text | Set by MOD-081 when reconciled against settlement file | |
| failure_reason | text | Set on FAILED; e.g. SPONSOR_REJECTED, VALIDATION_FAILED, POSTING_ERROR |
|
| return_reason_code | text | Scheme return code; set on RETURNED | |
| return_reason_text | text | Plain-language description from reason-mapper.ts; set on RETURNED |
|
| value_date | date | Next AEST business day when submitted past cut-off (17:00 AEST default); NULL if same-day settlement | |
| channel | text | NOT NULL CHECK (channel IN ('APP','API','BACK_OFFICE','BATCH')) | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | Touch trigger maintains |
Indexes:
- UNIQUE (idempotency_key) — defence-in-depth replay guard
- idx_bpay_payments_payment_id on (payment_id)
- idx_bpay_payments_biller_code on (biller_code, created_at DESC)
- idx_bpay_payments_status on (status) WHERE status NOT IN ('SETTLED','RETURNED','FAILED')
Notes: Uppercase status and channel enums are consistent with the rest of SD04. payment_id is NOT NULL — minted by MOD-119 before the row is inserted. No FK to payments.payments because that row is created by MOD-020 in the same synchronous chain. DISPUTED is in the enum but no v1 code path sets it — reserved for back-office tooling and future MOD-053 case management integration. posting_id and reversal_posting_id are application-layer only for the same cross-DB reason as payments.intra_bank_transfers.
payments.bpay_biller_cache¶
Cache of BPAY biller records refreshed daily from the sponsor bank's biller directory. Seeded from V900 migration in dev/uat with known test biller codes (PASS-, FAIL-, RETURN-* patterns per ADR-044). Owned by MOD-119.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| biller_code | text | NOT NULL UNIQUE | BPAY biller code |
| biller_name | text | NOT NULL | Full biller display name |
| crn_format | text | NOT NULL CHECK (crn_format IN ('LUHN','REGEX','FIXED_LENGTH','NONE')) | CRN validation algorithm |
| crn_regex | text | Populated when crn_format = 'REGEX' or 'FIXED_LENGTH' |
|
| crn_length | int | Populated when crn_format = 'FIXED_LENGTH' |
|
| min_amount | numeric(18,2) | Biller-declared minimum payment amount; NULL = no minimum | |
| max_amount | numeric(18,2) | Biller-declared maximum payment amount; NULL = no maximum | |
| accepted_amounts | jsonb | Biller-declared list of accepted specific amounts; NULL = any amount within min/max | |
| is_active | boolean | NOT NULL DEFAULT true | False = biller deregistered; lookup returns 404 |
| source | text | NOT NULL CHECK (source IN ('SPONSOR_DIRECTORY','DEV_SEED')) | |
| last_refreshed_at | timestamptz | NOT NULL | Timestamp of the most recent successful directory refresh for this record |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- UNIQUE (biller_code) — primary lookup key
- idx_bpay_biller_cache_active on (biller_code) WHERE is_active = true
Notes: UPSERT on biller_code on daily refresh (INSERT ... ON CONFLICT DO UPDATE). The refresh Lambda tolerates a 404/501 from MOD-157's stub in dev/uat — logs WARN, leaves cache unchanged. accepted_amounts is a jsonb array of numeric(18,2) strings for billers that only accept specific amounts (e.g. council rate instalments).
payments.payid_registrations¶
PayID proxy identifier registrations. One row per registered PayID. Customers register a mobile number, email address, or ABN as an NPP proxy identifier via MOD-120. NPP-directory provisioning is synchronous at registration time. AU-only.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| account_id | uuid | NOT NULL | Linked account; application-layer only (no FK — cross-DB to SD01) |
| party_id | uuid | NOT NULL | Cross-domain ref to SD02 party.parties(party_id) |
| payid_type | text | NOT NULL CHECK (payid_type IN ('MOBILE','EMAIL','ABN','ORGANISATION_ID')) | NPP PayID type |
| payid_value | text | NOT NULL | Encrypted at rest; the actual mobile number / email address / ABN |
| display_name | text | NOT NULL | Customer-visible name shown to PayID resolution callers |
| status | text | NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','SUSPENDED','DEREGISTERED')) | |
| sponsor_reference | text | Sponsor bank's NPP directory reference; set after successful provisioning | |
| failure_reason | text | Populated when provisioning or deregistration fails | |
| registration_idempotency_key | text | NOT NULL UNIQUE | Idempotency key for FR-541 register endpoint |
| trace_id | uuid | NOT NULL | ADR-031 |
| jurisdiction | char(2) | NOT NULL DEFAULT 'AU' CHECK (jurisdiction = 'AU') | NPP is AU-only |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | Touch trigger maintains |
| deregistered_at | timestamptz | Set on DEREGISTERED; per NPP Rule 4.7 deregister ≤ 2 minutes |
Indexes:
- UNIQUE (payid_type, payid_value) — primary lookup key for inbound PayID resolution; one value per type across the bank
- idx_payid_registrations_account_id on (account_id) WHERE status = 'ACTIVE'
- idx_payid_registrations_party_id on (party_id)
Notes: Mutable by design — PayID management lifecycle (suspend, reactivate, update display_name, deregister) requires UPDATE. Not append-only. Owned by MOD-120. AU-only (NPP scheme). SUSPENDED rows are retained for audit; DEREGISTERED rows are soft-deleted by status.
payments.osko_payments¶
Rail-specific payment record for NPP/Osko real-time payments. One row per payment attempt (OUTBOUND or INBOUND). Owned by MOD-120. AU-only. The audit chain is handled by MOD-020 (payment_events) + MOD-001 (posting_completed) + MOD-022 (SETTLEMENT_CONFIRMED / REVERSAL_CONFIRMED).
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL | Minted by MOD-120 at INSERT; passed to MOD-020 and MOD-001 for audit linkage. No FK — payments.payments row created by MOD-020 in the same synchronous chain |
| idempotency_key | text | NOT NULL | MOD-120 idempotency key; threads through MOD-020 and MOD-001 unchanged |
| trace_id | uuid | NOT NULL | ADR-031 |
| direction | text | NOT NULL CHECK (direction IN ('OUTBOUND','INBOUND')) | OUTBOUND: customer → NPP clearing account. INBOUND: NPP clearing account → customer |
| from_account_id | uuid | NOT NULL | Debit account; application-layer only (no FK — cross-DB to SD01) |
| to_account_id | uuid | Credit account; NULL for OUTBOUND to external PayID; application-layer only | |
| payid_value | text | PayID being paid to (OUTBOUND) or received via (INBOUND); NULL for account-number-addressed payments | |
| payid_type | text | CHECK (payid_type IN ('MOBILE','EMAIL','ABN','ORGANISATION_ID')) | NULL when payid_value is NULL |
| resolved_display_name | text | Name returned by PayID resolution at resolve time | |
| confirmed_display_name | text | Name shown to customer and confirmed; CON-005 evidence field | |
| name_confirmed | boolean | NOT NULL DEFAULT false | FR-542 — must be true before any OUTBOUND SUBMITTING transition; server-enforced |
| name_confirmed_at | timestamptz | Timestamp when customer confirmed the display name | |
| is_first_time_payee | boolean | NOT NULL DEFAULT false | Scam-Safe Accord — first time this customer has sent to this PayID |
| acknowledged_high_value | boolean | NOT NULL DEFAULT false | Scam-Safe Accord — customer acknowledged high-value first-time-payee warning; required when is_first_time_payee=true and amount > HIGH_VALUE_THRESHOLD_AUD |
| end_to_end_id | text | NOT NULL UNIQUE | Server-generated UUID; NPP end-to-end identifier threaded through MOD-020 and sponsor submission |
| npp_message_id | text | NPP scheme message ID returned by sponsor after submission | |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL DEFAULT 'AUD' | NPP is AUD-only |
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','SUBMITTING','PROCESSING','COMPLETED','RETURNED','DISPUTED','FAILED')) | SUBMITTING = between MOD-001 debit commit and sponsor NPP submission |
| posting_id | uuid | Set on SUBMITTING; debit posting reference. Application-layer only (no FK) | |
| reversal_posting_id | uuid | Set on RETURNED; credit-back posting reference. Application-layer only | |
| sponsor_reference | text | Sponsor bank's NPP reference; set after sponsor accepts submission | |
| return_reason_code | text | NPP scheme return code; set on RETURNED | |
| return_reason_text | text | Plain-language description from reason-mapper.ts; set on RETURNED | |
| sender_display_name | text | INBOUND only: sender's display name from NPP message | |
| jurisdiction | char(2) | NOT NULL DEFAULT 'AU' CHECK (jurisdiction = 'AU') | NPP is AU-only |
| channel | text | NOT NULL CHECK (channel IN ('APP','API','BACK_OFFICE')) | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | Touch trigger maintains |
Indexes:
- UNIQUE (idempotency_key) — defence-in-depth replay guard
- UNIQUE (end_to_end_id) — NPP end-to-end identifier uniqueness
- idx_osko_payments_payment_id on (payment_id)
- idx_osko_payments_from_account_id on (from_account_id)
- idx_osko_payments_status on (status) WHERE status NOT IN ('COMPLETED','RETURNED','FAILED')
- idx_osko_payments_payid_value on (payid_value) WHERE payid_value IS NOT NULL
Notes: Append-then-update lifecycle (INSERT at PENDING, UPDATE through SUBMITTING → PROCESSING → COMPLETED / RETURNED / FAILED). Not immutable — same pattern as payments.bpay_payments. DISPUTED is in the enum but no v1 code path sets it — reserved for MOD-053 v2 case management integration. posting_id and reversal_posting_id are application-layer only (no FK — cross-DB to SD01). AML-005 LOG surface — full TM-relevant metadata including sender/receiver account IDs, PayID, name-confirmation evidence, and Scam-Safe Accord flags.
payments.nz_interbank_payments¶
Rail-specific payment record for NZ interbank payments cleared via the Payments NZ clearing network. One row per payment attempt. Owned by MOD-122. NZ-only. The audit chain is handled by MOD-020 (payment_events) + MOD-001 (posting_completed) + MOD-022 (SETTLEMENT_CONFIRMED / REVERSAL_CONFIRMED).
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL | Minted by MOD-122 at INSERT; passed to MOD-020 and MOD-001 for audit linkage. No FK — payments.payments row created by MOD-020 in the same synchronous chain |
| idempotency_key | text | NOT NULL | MOD-122 idempotency key; threads through MOD-020 and MOD-001 unchanged |
| trace_id | uuid | NOT NULL | ADR-031 |
| from_account_id | uuid | Debit account; application-layer only (no FK — cross-DB to SD01). NULL for INBOUND direction payments | |
| to_account_number | text | NOT NULL | NZ account in XX-XXXX-XXXXXXX-XX format (bank-branch-account-suffix); modular-weight checksum validated before MOD-020 pre-payment gate |
| to_account_name | text | NOT NULL | Beneficiary account name |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL DEFAULT 'NZD' CHECK (currency = 'NZD') | Payments NZ is NZD-only |
| direction | text | NOT NULL CHECK (direction IN ('OUTBOUND','INBOUND')) | |
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','SUBMITTING','SUBMITTED','SETTLED','RETURNED','FAILED')) | SUBMITTING = between MOD-001 debit commit and Payments NZ sponsor submission |
| posting_id | uuid | Set on SUBMITTING; debit posting reference. Application-layer only (no FK) | |
| reversal_posting_id | uuid | Set on RETURNED; credit-back posting reference. Application-layer only | |
| clearing_reference | text | Payments NZ clearing network reference; set after scheme accepts the instruction | |
| sponsor_reference | text | Sponsor bank's reference for the submission | |
| return_reason_code | text | Payments NZ scheme return code; set on RETURNED | |
| return_reason_text | text | Plain-language description from reason-mapper.ts; set on RETURNED |
|
| end_to_end_id | text | UNIQUE | Payments NZ end-to-end identifier threaded through MOD-020 and sponsor submission |
| cut_off_applied | boolean | NOT NULL DEFAULT false | True when submission was deferred to next business day due to CUT_OFF_TIME_NZST |
| value_date | date | Business day of settlement; NULL if same-day | |
| channel | text | NOT NULL CHECK (channel IN ('APP','API','BACK_OFFICE')) | |
| jurisdiction | char(2) | NOT NULL DEFAULT 'NZ' CHECK (jurisdiction = 'NZ') | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | Touch trigger maintains |
Indexes:
- UNIQUE (idempotency_key) — defence-in-depth replay guard
- UNIQUE (end_to_end_id) WHERE end_to_end_id IS NOT NULL — Payments NZ end-to-end identifier uniqueness
- idx_nz_interbank_payments_payment_id on (payment_id)
- idx_nz_interbank_payments_to_account_number on (to_account_number, created_at DESC)
- idx_nz_interbank_payments_status on (status) WHERE status NOT IN ('SETTLED','RETURNED','FAILED')
Notes: Append-then-update lifecycle (INSERT at PENDING, UPDATE through SUBMITTING → SUBMITTED → SETTLED / RETURNED / FAILED). NZ account format XX-XXXX-XXXXXXX-XX (bank-branch-account-suffix); modular-weight checksum validated before MOD-020 pre-payment gate. payment_id is NOT NULL — minted by MOD-122 before INSERT. posting_id and reversal_posting_id are application-layer only (no FK — cross-DB to SD01). Cut-off logic controlled by CUT_OFF_TIME_NZST env var (default 15:00 NZST); weekend-only business-day math in v1; NZ public holiday awareness is a v2 gap. direction = 'INBOUND' rows are written on receipt of inbound credit notifications from the Payments NZ sponsor webhook.
payments.payment_limits¶
Per-customer, per-payment-type daily and per-transaction limits enforced by MOD-021. A customer may have a set of limits derived from product defaults, CDD tier, and any manual overrides.
| 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) |
| payment_type | text | NOT NULL | Payment type this limit applies to (or 'ALL') |
| limit_type | text | NOT NULL CHECK (limit_type IN ('PER_TRANSACTION','DAILY','WEEKLY','MONTHLY','ROLLING_30_DAY','APPROVAL_THRESHOLD')) | ROLLING_30_DAY = 30-day rolling window velocity limit; APPROVAL_THRESHOLD = per-transaction amount above which a multi-party authorisation is required (FR-127). Added by MOD-021 V002. |
| limit_amount | numeric(18,2) | NOT NULL | |
| currency | char(3) | NOT NULL | |
| jurisdiction | char(2) | NOT NULL | |
| channel | text | NOT NULL DEFAULT 'ALL' CHECK (channel IN ('APP','API','OPEN_BANKING','AGENT','ALL')) | Channel this limit applies to. ALL applies to all channels. Added by MOD-021 V002. |
| limit_source | text | NOT NULL CHECK (limit_source IN ('PRODUCT_DEFAULT','CDD_TIER','MANUAL_OVERRIDE','REGULATORY')) | |
| effective_from | timestamptz | NOT NULL DEFAULT now() | |
| effective_to | timestamptz | CHECK (effective_to IS NULL OR effective_to > effective_from) | NULL = currently active |
| set_by | text | NOT NULL | Staff ID or module that set this limit |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_payment_limits_party_active on (party_id, payment_type) WHERE effective_to IS NULL
- idx_payment_limits_party_id on (party_id)
payments.limit_change_audit (MOD-021)¶
Immutable audit record of every limit configuration change. Written by MOD-021 on every successful PUT /limits operation (FR-126). Required for AML-005 compliance — operator changes to limits must be auditable. Append-only.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| party_id | uuid | NOT NULL | Party whose limit was changed |
| payment_type | text | NOT NULL | |
| limit_type | text | NOT NULL | |
| previous_limit_amount | numeric(18,2) | NULL if this is the first limit record for this combination | |
| new_limit_amount | numeric(18,2) | NOT NULL | |
| currency | char(3) | NOT NULL | |
| channel | text | NOT NULL | |
| changed_by | text | NOT NULL | Staff ID or module that made the change |
| change_reason | text | NOT NULL | Mandatory justification — FR-126 |
| idempotency_key | text | NOT NULL UNIQUE | |
| trace_id | text | ||
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_limit_change_audit_party_id on (party_id, created_at DESC)
payments.idempotency_keys (MOD-021, shared SD04 surface)¶
Shared idempotency table for all SD04 handlers that need deduplicated request processing. Populated by the limits-check Lambda and limits-set Lambda in MOD-021. Future SD04 modules should reuse this table rather than creating per-module idempotency tables. DDL owned by MOD-021.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| idempotency_key | text | NOT NULL UNIQUE | Caller-supplied key |
| module_id | text | NOT NULL | Owning module (e.g. MOD-021) |
| handler | text | NOT NULL | Handler name (e.g. limits-check) |
| response_status | integer | NOT NULL | HTTP status code of the original response |
| response_body | jsonb | NOT NULL DEFAULT '{}' | Cached response for replay |
| expires_at | timestamptz | NOT NULL | Key is eligible for sweeper deletion after this timestamp |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_idempotency_keys_key on (idempotency_key) — unique lookup
- idx_idempotency_keys_expires_at on (expires_at) — sweeper
payments.fx_rates¶
Published FX rates sourced from the bank's treasury feed. MOD-025 reads this table to lock rates for conversions. Rates are inserted by the treasury rate ingestion process; they are never updated.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| from_currency | char(3) | NOT NULL | |
| to_currency | char(3) | NOT NULL | |
| rate | numeric(16,8) | NOT NULL CHECK (rate > 0) | Mid-market rate (buy/sell spread applied by MOD-025 at lock time) |
| rate_type | text | NOT NULL CHECK (rate_type IN ('MID','BUY','SELL','RETAIL')) | |
| spread_bps | int | NOT NULL DEFAULT 0 | Basis points spread applied to derive buy/sell from mid |
| published_at | timestamptz | NOT NULL | Rate publication timestamp from the treasury feed |
| valid_until | timestamptz | NOT NULL CHECK (valid_until > published_at) | Rate expiry (typically seconds/minutes ahead) |
| source | text | NOT NULL | Rate feed source identifier |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_fx_rates_pair_published on (from_currency, to_currency, published_at DESC)
- idx_fx_rates_valid_until on (valid_until) WHERE valid_until > now()
Notes: Rate rows are immutable once inserted — enforced by trg_fx_rates_immutable (ADR-048 Category 1). Treasury rate corrections require a new row with an updated published_at; the old row is not deleted.
payments.fx_locks¶
A rate lock record created by MOD-025 when a customer initiates an FX conversion. Locks are valid for 30–60 seconds. Once a payment settles using the lock, used_at is populated; unused locks expire.
| 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) |
| from_currency | char(3) | NOT NULL | |
| to_currency | char(3) | NOT NULL | |
| locked_rate | numeric(16,8) | NOT NULL | Exact rate locked for this conversion |
| from_amount | numeric(18,2) | NOT NULL | Amount to be converted from source currency |
| to_amount | numeric(18,2) | NOT NULL | Guaranteed to-currency amount at locked rate |
| locked_at | timestamptz | NOT NULL DEFAULT now() | |
| expires_at | timestamptz | NOT NULL CHECK (expires_at > locked_at) | Lock expiry (locked_at + 30–60 seconds) |
| used_at | timestamptz | Set when the payment using this lock settles | |
| payment_id | uuid | REFERENCES payments.payments(id) | Linked payment |
| rate_source_id | uuid | REFERENCES payments.fx_rates(id) | Rate row used to derive this lock |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_fx_locks_party_id on (party_id)
- idx_fx_locks_expires_unused on (expires_at) WHERE used_at IS NULL
- idx_fx_locks_payment_id on (payment_id) WHERE payment_id IS NOT NULL
payments.fx_lock_audit (MOD-025)¶
Append-only lifecycle log for FX rate locks. One row per lifecycle event (ISSUED, CONSUMED, EXPIRED). Immutable per ADR-048 Cat 1 — UPDATE/DELETE/TRUNCATE blocked by V003 trigger.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| fx_lock_id | uuid | NOT NULL | References payments.fx_locks(id) |
| party_id | uuid | NOT NULL | Cross-domain ref to SD02 party.parties(party_id) |
| event_type | text | NOT NULL CHECK (ISSUED, CONSUMED, EXPIRED) |
Lifecycle stage |
| from_currency | char(3) | NOT NULL | |
| to_currency | char(3) | NOT NULL | |
| locked_rate | numeric(16,8) | NOT NULL | |
| mid_rate | numeric(16,8) | NOT NULL | Mid-market rate at lock time |
| spread_bps | int | NOT NULL | |
| from_amount | numeric(18,2) | NOT NULL | |
| to_amount | numeric(18,2) | NOT NULL | |
| rate_source | text | NOT NULL | |
| rate_source_id | uuid | References payments.fx_rates(id) |
|
| locked_at | timestamptz | Populated on ISSUED | |
| expires_at | timestamptz | Populated on ISSUED | |
| consumed_at | timestamptz | Populated on CONSUMED | |
| payment_id | uuid | Populated on CONSUMED | |
| conversion_id | uuid | Populated on CONSUMED | |
| source_posting_id | uuid | Populated on CONSUMED | |
| target_posting_id | uuid | Populated on CONSUMED | |
| expired_at | timestamptz | Populated on EXPIRED | |
| trace_id | uuid | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- idx_fx_lock_audit_party_id on (party_id, created_at DESC)
- idx_fx_lock_audit_fx_lock_id on (fx_lock_id)
- idx_fx_lock_audit_event_type on (event_type, created_at DESC)
Immutability: trg_fx_lock_audit_no_update, trg_fx_lock_audit_no_delete, trg_fx_lock_audit_no_truncate — ADR-048 Cat 1.
payments.ifti_cmir_queue¶
Pre-submission queue for IFTI (AU) and CMIR (NZ) threshold reports identified by MOD-026. Records are created when a cross-border payment crosses the reporting threshold. MOD-019 in SD03 consumes this queue to generate the actual regulatory submission.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL REFERENCES payments.payments(id) | |
| party_id | uuid | NOT NULL | cross-domain ref to SD02 party.parties(party_id) |
| report_type | text | NOT NULL CHECK (report_type IN ('IFTI','CMIR')) | |
| jurisdiction | char(2) | NOT NULL | |
| threshold_amount | numeric(18,2) | NOT NULL | Threshold that triggered the report obligation |
| threshold_currency | char(3) | NOT NULL | |
| queue_status | text | NOT NULL CHECK (queue_status IN ('QUEUED','PROCESSING','SUBMITTED','FAILED')) | |
| triggered_at | timestamptz | NOT NULL DEFAULT now() | When MOD-026 identified the threshold crossing |
| submission_id | uuid | Soft reference to SD03 aml.regulatory_submissions(id); back-populated by MOD-019 after filing; no FK constraint (cross-domain async write from bank-aml) |
|
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_ifti_cmir_queue_status on (queue_status) WHERE queue_status IN ('QUEUED','PROCESSING')
- idx_ifti_cmir_queue_payment_id on (payment_id)
- idx_ifti_cmir_queue_triggered_at on (triggered_at DESC)
payments.settlement_files¶
Records of settlement file submissions to payment rails (NPP, SWIFT, RTGS, direct entry). One row per batch or individual settlement file sent to a rail. Used by MOD-081 for reconciliation.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| rail | text | NOT NULL CHECK (rail IN ('NPP','RTGS','SWIFT','DIRECT_ENTRY','BPAY','CARD')) | |
| jurisdiction | char(2) | NOT NULL | |
| file_reference | text | NOT NULL UNIQUE | Bank-generated file reference |
| payment_count | int | NOT NULL | Number of payments in this file |
| total_amount | numeric(18,2) | NOT NULL | |
| currency | char(3) | NOT NULL | |
| file_status | text | NOT NULL CHECK (file_status IN ('GENERATED','SUBMITTED','ACKNOWLEDGED','SETTLED','PARTIALLY_SETTLED','REJECTED')) | |
| generated_at | timestamptz | NOT NULL DEFAULT now() | |
| submitted_at | timestamptz | ||
| settled_at | timestamptz | ||
| s3_key | text | S3 object key for the settlement file | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_settlement_files_file_status on (file_status) WHERE file_status NOT IN ('SETTLED','REJECTED')
- idx_settlement_files_rail_generated on (rail, generated_at DESC)
- idx_settlement_files_file_reference on (file_reference)
payments.reconciliation_exceptions¶
Payments or settlement items that MOD-081 could not match to a settlement confirmation, or that settled for an amount different from instructed. Requires analyst resolution.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | REFERENCES payments.payments(id) | NULL for inbound unmatched items |
| settlement_file_id | uuid | REFERENCES payments.settlement_files(id) | |
| exception_type | text | NOT NULL CHECK (exception_type IN ('UNMATCHED_OUTBOUND','SHORT_SETTLED','OVER_SETTLED','DUPLICATE','MISSING_RETURN','UNMATCHED_INBOUND')) | |
| instructed_amount | numeric(18,2) | ||
| settled_amount | numeric(18,2) | ||
| variance | numeric(18,2) | instructed_amount - settled_amount | |
| currency | char(3) | NOT NULL | |
| exception_status | text | NOT NULL CHECK (exception_status IN ('OPEN','UNDER_REVIEW','RESOLVED','ESCALATED')) | |
| detected_at | timestamptz | NOT NULL DEFAULT now() | |
| resolved_at | timestamptz | ||
| resolved_by | text | ||
| resolution_notes | text | ||
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_recon_exceptions_status on (exception_status) WHERE exception_status IN ('OPEN','UNDER_REVIEW')
- idx_recon_exceptions_detected_at on (detected_at DESC)
- idx_recon_exceptions_payment_id on (payment_id) WHERE payment_id IS NOT NULL
payments.nostro_positions¶
Current and intraday positions for correspondent bank (nostro) accounts, maintained by MOD-082. Used to gate high-value payments when nostro funding is insufficient.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| correspondent_bank | text | NOT NULL | BIC or internal code of the correspondent |
| nostro_account_ref | text | NOT NULL | Account reference at the correspondent |
| currency | char(3) | NOT NULL | |
| opening_balance | numeric(18,2) | NOT NULL DEFAULT 0.00 | |
| current_balance | numeric(18,2) | NOT NULL DEFAULT 0.00 | Updated intraday |
| available_balance | numeric(18,2) | NOT NULL DEFAULT 0.00 | After pending outflows |
| minimum_threshold | numeric(18,2) | NOT NULL DEFAULT 0.00 | Below this, auto-funding is triggered |
| maximum_threshold | numeric(18,2) | NULL | Above this, over-funding alert is raised (FR-162). Added by MOD-082 V001. |
| as_of | timestamptz | NOT NULL | Timestamp of the most recent position update |
| position_date | date | NOT NULL | Business date this position belongs to |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_nostro_positions_currency_date on (currency, position_date DESC)
- idx_nostro_positions_correspondent on (correspondent_bank, position_date)
payments.nostro_adjustment_audit (MOD-082)¶
Append-only log of every manual adjustment to a nostro position. One row per adjustment event. Captures full before/after balance snapshot so the record is self-contained for audit reconstruction without joining the live nostro_positions table. Immutable per ADR-048 Cat 1 — UPDATE/DELETE/TRUNCATE blocked by V003 trigger.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| nostro_position_id | uuid | NOT NULL | References payments.nostro_positions(id) |
| correspondent_bank | text | NOT NULL | |
| nostro_account_ref | text | NOT NULL | |
| currency | char(3) | NOT NULL | |
| adjustment_type | text | NOT NULL CHECK (FUNDING, DRAIN, THRESHOLD_CHANGE, CORRECTION, FX_REVALUATION) |
|
| amount_delta | numeric(18,2) | NOT NULL | Signed delta; positive for FUNDING, negative for DRAIN, 0 for THRESHOLD_CHANGE |
| previous_current_balance | numeric(18,2) | NOT NULL | Balance before this adjustment |
| previous_available_balance | numeric(18,2) | NOT NULL | |
| previous_minimum_threshold | numeric(18,2) | NOT NULL | |
| previous_maximum_threshold | numeric(18,2) | NULL if not set before adjustment | |
| new_current_balance | numeric(18,2) | NOT NULL | Balance after this adjustment |
| new_available_balance | numeric(18,2) | NOT NULL | |
| new_minimum_threshold | numeric(18,2) | NOT NULL | |
| new_maximum_threshold | numeric(18,2) | ||
| reason_code | text | NOT NULL | Machine key e.g. CORR_BANK_TOPUP, RECON_BREAK_CORRECTION |
| reason_notes | text | Human notes | |
| operator_id | text | NOT NULL | Asserted staff identity (cross-domain; MOD-068 authorisation wired in future) |
| operator_role | text | NOT NULL | e.g. TREASURY_SENIOR |
| trace_id | uuid | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Immutability: trg_nostro_adjustment_audit_immutable — ADR-048 Cat 1.
payments.nostro_position_eod_snapshots (MOD-082)¶
Daily end-of-day snapshots for each nostro corridor (FR-163). One row per (correspondent_bank, nostro_account_ref, currency, snapshot_date). Immutable per ADR-048 Cat 1 — UPDATE/DELETE/TRUNCATE blocked by V005 trigger.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| snapshot_date | date | NOT NULL | Business date |
| jurisdiction | char(2) | NOT NULL CHECK (NZ, AU) |
|
| correspondent_bank | text | NOT NULL | |
| nostro_account_ref | text | NOT NULL | |
| currency | char(3) | NOT NULL | |
| opening_balance | numeric(18,2) | NOT NULL | |
| closing_balance | numeric(18,2) | NOT NULL | |
| closing_available | numeric(18,2) | NOT NULL | |
| minimum_threshold | numeric(18,2) | NOT NULL | Snapshot of threshold at EOD |
| maximum_threshold | numeric(18,2) | ||
| open_settlement_count | int | NOT NULL DEFAULT 0 | FR-163 open settlement obligations |
| open_settlement_total | numeric(18,2) | NOT NULL DEFAULT 0.00 | |
| base_currency | char(3) | NOT NULL DEFAULT 'NZD' | |
| unrealised_pnl_base | numeric(18,2) | NOT NULL DEFAULT 0.00 | Unrealised P&L in base currency |
| fx_rate_applied | numeric(16,8) | Rate used for base-currency conversion | |
| rate_source | text | MOD-085 rate source reference | |
| snapshot_taken_at | timestamptz | NOT NULL DEFAULT now() | |
| trace_id | uuid | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Unique constraint: (snapshot_date, correspondent_bank, nostro_account_ref, currency)
Indexes:
- idx_nostro_eod_snap_date on (snapshot_date DESC, jurisdiction)
- idx_nostro_eod_snap_currency on (currency, snapshot_date DESC)
Immutability: trg_nostro_position_eod_snapshots_immutable — ADR-048 Cat 1.
payments.open_banking_consents¶
Records of Open Banking / CDR consents granted by customers, managed by MOD-061 (AU CDR) and the NZ Open Banking gateway. Each row represents a granted consent scope for a data recipient.
| 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) |
| data_recipient_id | text | NOT NULL | Accredited data recipient identifier |
| consent_type | text | NOT NULL CHECK (consent_type IN ('CDR_AU','NZ_OPEN_BANKING')) | |
| scopes | jsonb | NOT NULL | Granted consent scopes (e.g. ["bank:accounts.basic:read","bank:transactions:read"]) |
| consent_status | text | NOT NULL CHECK (consent_status IN ('ACTIVE','WITHDRAWN','EXPIRED','REVOKED')) | |
| granted_at | timestamptz | NOT NULL DEFAULT now() | |
| expires_at | timestamptz | NOT NULL | CDR maximum 12-month consent duration |
| withdrawn_at | timestamptz | ||
| withdrawal_reason | text | ||
| sharing_duration_days | int | NOT NULL | Customer-selected sharing period |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_open_banking_consents_party_active on (party_id) WHERE consent_status = 'ACTIVE'
- idx_open_banking_consents_expires_at on (expires_at) WHERE consent_status = 'ACTIVE'
- idx_open_banking_consents_recipient_party on (data_recipient_id, party_id)
payments.device_intelligence (MOD-024)¶
Per-device aggregated fraud-grade profile. One row per device_fingerprint_hash. Mutable — counters, trust score, and flagged status are updated on every observe call and every anomaly event. Distinct from MOD-068's access.device_registry (auth-grade trust-for-login record); both are keyed by the same device_fingerprint_hash for cross-correlation.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| device_fingerprint_hash | text | NOT NULL UNIQUE | SHA-256 of device fingerprint fields; same key used by MOD-068 access.device_registry |
| customer_id | uuid | NOT NULL | First customer to use this device; cross-domain ref to SD02 |
| os_version | text | ||
| app_version | text | ||
| screen_resolution | text | ||
| network_type | text | e.g. WIFI, CELLULAR_4G, CELLULAR_5G | |
| emulator_detected | boolean | NOT NULL DEFAULT false | FR-139 emulator flag |
| rooted_device | boolean | NOT NULL DEFAULT false | FR-139 rooted/jailbroken flag |
| trust_score | int | NOT NULL DEFAULT 100 CHECK (trust_score BETWEEN 0 AND 100) | Decremented on anomaly; incremented on clean sessions |
| flagged_as_fraudulent | boolean | NOT NULL DEFAULT false | Set true when bank.payments.fraud_alert_raised received from MOD-023 |
| first_seen_at | timestamptz | NOT NULL DEFAULT now() | |
| last_seen_at | timestamptz | NOT NULL DEFAULT now() | |
| observe_count | int | NOT NULL DEFAULT 1 | Cumulative observe API calls from this device |
| anomaly_count | int | NOT NULL DEFAULT 0 | Cumulative anomalies detected |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: idx_device_intelligence_hash on (device_fingerprint_hash) — UNIQUE; idx_device_intelligence_customer on (customer_id); idx_device_intelligence_flagged partial WHERE flagged_as_fraudulent = true.
payments.session_observations (MOD-024)¶
Per-session device fingerprint snapshot captured on every observe API call. FR-140 7-year retention. Append-only — ADR-048 Cat 1 immutable.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| session_id | uuid | NOT NULL | Cross-reference to MOD-068 session |
| customer_id | uuid | NOT NULL | |
| device_fingerprint_hash | text | NOT NULL REFERENCES payments.device_intelligence(device_fingerprint_hash) | |
| os_version | text | ||
| app_version | text | ||
| screen_resolution | text | ||
| network_type | text | ||
| ip_region | text | Coarse geolocation; from MOD-068 session_created event | |
| emulator_detected | boolean | NOT NULL DEFAULT false | |
| rooted_device | boolean | NOT NULL DEFAULT false | |
| observed_at | timestamptz | NOT NULL DEFAULT now() | |
| trace_id | uuid | ||
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: idx_session_obs_session_id on (session_id); idx_session_obs_device_hash on (device_fingerprint_hash); idx_session_obs_customer_observed on (customer_id, observed_at DESC).
Immutability: trg_session_observations_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-024 V001). ADR-048 Cat 1. FR-140 7-year retention enforced via Snowflake CDC (MOD-042).
payments.device_anomalies (MOD-024)¶
Per-anomaly record. AML-005 LOG surface. Append-only — ADR-048 Cat 1 immutable.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | Referenced as anomaly_id in the device_anomaly_detected event |
| session_id | uuid | NOT NULL | |
| customer_id | uuid | NOT NULL | |
| device_fingerprint_hash | text | NOT NULL | |
| anomaly_type | text | NOT NULL CHECK (anomaly_type IN ('NEW_DEVICE','KNOWN_FRAUD_DEVICE','IMPOSSIBLE_TRAVEL','EMULATOR_DETECTED','ROOTED_DEVICE')) | |
| severity | text | NOT NULL CHECK (severity IN ('INFO','WARN','CRITICAL')) | |
| action_recommended | text | NOT NULL CHECK (action_recommended IN ('NONE','STEP_UP','BLOCK')) | |
| detail | jsonb | NOT NULL DEFAULT '{}' | Anomaly-type-specific detail (e.g. distance_km for IMPOSSIBLE_TRAVEL) |
| detected_at | timestamptz | NOT NULL DEFAULT now() | |
| trace_id | uuid | ||
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: idx_device_anomalies_session on (session_id); idx_device_anomalies_customer_detected on (customer_id, detected_at DESC); idx_device_anomalies_type_severity on (anomaly_type, severity).
Immutability: trg_device_anomalies_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-024 V001). ADR-048 Cat 1. AML-005 LOG surface; AML behavioural model feed via CDC to Snowflake (MOD-042).
payments.fraud_scores (MOD-023)¶
Append-only fraud scoring record for every payment submitted. One row per (payment_id, model_version) scoring run. DT-005 LOG surface — captures every input feature, weight snapshot, and output score for model audit. ADR-048 Cat 1 immutable.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| payment_id | uuid | NOT NULL REFERENCES payments.payments(id) | |
| customer_id | uuid | NOT NULL | Cross-domain ref to SD02 party.parties(party_id) |
| score | int | NOT NULL CHECK (score BETWEEN 0 AND 1000) | Composite fraud score |
| action | text | NOT NULL CHECK (action IN ('PASS','STEP_UP','BLOCK')) | Outcome of threshold evaluation |
| model_version | text | NOT NULL | Scorer version e.g. rule-v1.0.0 |
| feature_weights | jsonb | NOT NULL | Named weight constants snapshot at score time (DT-005 audit) |
| input_features | jsonb | NOT NULL | Raw feature values used in this scoring run |
| threshold_warn | int | NOT NULL | AppConfig WARN threshold applied |
| threshold_block | int | NOT NULL | AppConfig BLOCK threshold applied |
| idempotency_key | text | NOT NULL UNIQUE | Prevents duplicate scoring of the same payment |
| trace_id | uuid | ADR-031 propagation | |
| scored_at | timestamptz | NOT NULL DEFAULT now() | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: (payment_id) UNIQUE (one score per payment in v1); (customer_id, scored_at DESC); (action, scored_at DESC) partial WHERE action <> 'PASS'.
Immutability: trg_fraud_scores_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-023 V001). ADR-048 Cat 1. DT-005 LOG compliance requires the full scoring context to be preserved for model audit.
payments.scam_scored_payees (MOD-023)¶
Mutable scam-payee cache for the CAP-040 score-payee API. One row per BSB+account number or NZ bank number. v1 is a stub — is_scam=false for everything not in the dev seed. When MOD-149 ships, an EventBridge consumer upserts rows from bank.scam.scam_database_updated.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| account_identifier | text | NOT NULL UNIQUE | Normalised BSB+account or NZ bank number |
| is_scam | boolean | NOT NULL DEFAULT false | True when confirmed scam payee |
| reason_code | text | NOT NULL | e.g. SCAM_DB_CONFIRMED / NO_SCAM_DB_AVAILABLE |
| source | text | NOT NULL CHECK (source IN ('SCAM_DATABASE','MANUAL_FLAG','DEV_SEED')) | |
| flagged_at | timestamptz | When first flagged | |
| last_updated | timestamptz | NOT NULL DEFAULT now() | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: (account_identifier) UNIQUE; (is_scam) partial WHERE is_scam = true.
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-payments.
Immutability triggers¶
| Table | Trigger | Category |
|---|---|---|
payments.payment_events |
trg_payment_events_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() |
Cat 1 |
payments.fx_rates |
trg_fx_rates_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() |
Cat 1 |
payments.fx_lock_audit |
trg_fx_lock_audit_no_update, trg_fx_lock_audit_no_delete, trg_fx_lock_audit_no_truncate — MOD-025 V003 |
Cat 1 |
payments.nostro_adjustment_audit |
trg_nostro_adjustment_audit_immutable — MOD-082 V003 |
Cat 1 |
payments.nostro_position_eod_snapshots |
trg_nostro_position_eod_snapshots_immutable — MOD-082 V005 |
Cat 1 |
payments.session_observations |
trg_session_observations_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-024 V001) |
Cat 1 |
payments.device_anomalies |
trg_device_anomalies_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-024 V001) |
Cat 1 |
payments.fraud_scores |
trg_fraud_scores_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() (MOD-023 V001). DT-005 LOG compliance; FR-136. |
Cat 1 |
Other tables (payments.payments, payments.payment_limits, payments.fx_locks, payments.settlement_files, payments.reconciliation_exceptions, payments.nostro_positions, payments.open_banking_consents, payments.ifti_cmir_queue, payments.device_intelligence, payments.scam_scored_payees, payments.bpay_payments, payments.bpay_biller_cache, payments.payid_registrations, payments.osko_payments) are mutable by design — payment status, limit lifecycle, settlement state, device trust profiles, biller cache, PayID management lifecycle, and Osko payment status transitions are updated by the owning modules.
CHECK constraints¶
| Table | Column | Constraint |
|---|---|---|
payments.payments |
amount |
CHECK (amount > 0) |
payments.fx_rates |
rate |
CHECK (rate > 0) |
payments.fx_rates |
valid_until |
CHECK (valid_until > published_at) |
payments.payment_limits |
effective_to |
CHECK (effective_to IS NULL OR effective_to > effective_from) |
payments.fx_locks |
expires_at |
CHECK (expires_at > locked_at) |
payments.bpay_payments |
amount |
CHECK (amount > 0) |
payments.payid_registrations |
payid_type |
CHECK (payid_type IN ('MOBILE','EMAIL','ABN','ORGANISATION_ID')) |
payments.payid_registrations |
jurisdiction |
CHECK (jurisdiction = 'AU') |
payments.osko_payments |
amount |
CHECK (amount > 0) |
payments.osko_payments |
direction |
CHECK (direction IN ('OUTBOUND','INBOUND')) |
payments.osko_payments |
jurisdiction |
CHECK (jurisdiction = 'AU') |
Not DB-enforced (Category 3 — cross-service or config-driven)¶
| Rule | Reason | Owner |
|---|---|---|
| Payment limit velocity check | Requires aggregating today's settled amounts across multiple rows — not a single-row invariant | MOD-021 Lambda |
| Sanctions gate | Depends on real-time sanctions service response; cross-service call | MOD-020 Lambda |
| Fraud score threshold | AppConfig-configurable; threshold is not constant | MOD-023 Lambda |
Direct debit mandate tables (MOD-114)¶
Three new tables owned by MOD-114. All dual-jurisdiction (NZ DDR + AU BECS); jurisdiction is a runtime context per ADR-042. Flyway migrations V001–V003.
payments.direct_debit_mandates (mutable — V001)¶
One row per mandate. Status transitions: ACTIVE → CANCELLED | SUSPENDED | EXPIRED. Partial unique index enforces one active mandate per account-biller-jurisdiction combination.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| mandate_id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| party_id | uuid | NOT NULL | Soft ref to SD02 kyc.parties(id) — no FK (cross-DB) |
| account_id | uuid | NOT NULL | Soft ref to SD01 accounts.accounts(id) — no FK (cross-DB) |
| jurisdiction | text | NOT NULL CHECK (jurisdiction IN ('NZ','AU')) | Runtime context per ADR-042 |
| biller_id | text | NOT NULL | Scheme-assigned biller identifier (NZ DDR biller code or AU APCA ID) |
| biller_name | text | NOT NULL | Human-readable biller name — PII; never emitted in logs per ADR-031 |
| scheme_ref | text | DDR reference (NZ) or APCA mandate ID (AU); null until confirmed by scheme | |
| max_amount | numeric(18,2) | Customer-set maximum per debit; null = no cap | |
| frequency | text | CHECK (frequency IN ('WEEKLY','FORTNIGHTLY','MONTHLY','VARIABLE')) | |
| status | text | NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','CANCELLED','SUSPENDED','EXPIRED')) | Uppercase enum |
| effective_date | date | NOT NULL | First date a debit presentation is valid |
| expiry_date | date | Null = open-ended | |
| cancelled_at | timestamptz | Set on cancellation | |
| cancel_reason | text | Customer or system reason | |
| trace_id | text | NOT NULL | Propagated from creation request |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL DEFAULT now() |
Indexes:
- UNIQUE (account_id, biller_id, jurisdiction) WHERE status = 'ACTIVE' — enforces one active mandate per account-biller-jurisdiction
- INDEX on (account_id, status)
- INDEX on (biller_id, status)
Grants: SELECT, INSERT, UPDATE to payments_app_user. No DELETE grant — status transitions replace deletion.
payments.direct_debit_events (immutable — V002, ADR-048 Cat 1)¶
Append-only audit log of all mandate and presentation lifecycle events. Satisfies PAY-002 LOG and NZ DDR / AU BECS scheme record-keeping requirements. Immutability enforced by trg_direct_debit_events_immutable (BEFORE UPDATE OR DELETE trigger calling fn_immutable_row()).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| mandate_id | uuid | NOT NULL REFERENCES payments.direct_debit_mandates(mandate_id) | |
| account_id | uuid | NOT NULL | Denormalised for query performance |
| party_id | uuid | NOT NULL | Denormalised |
| jurisdiction | text | NOT NULL | NZ or AU |
| event_type | text | NOT NULL CHECK (event_type IN ('MANDATE_CREATED','MANDATE_AMENDED','MANDATE_CANCELLED','MANDATE_SUSPENDED','MANDATE_EXPIRED','DEBIT_PRESENTED','DEBIT_AUTHORISED','DEBIT_DISHONOURED','DEBIT_DISPUTED')) | |
| before_state | jsonb | Mandate snapshot before the transition; null on MANDATE_CREATED | |
| after_state | jsonb | Mandate snapshot after the transition | |
| presentation_id | uuid | FK to payments.direct_debit_presentations; populated on DEBIT_* events | |
| actor | text | NOT NULL | customer:{party_id}, system:mod-114, staff:{staff_id}, provider:{biller_id} |
| policy_refs | jsonb | ["PAY-001","PAY-002","FR-506"] — traceability |
|
| trace_id | text | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Grants: SELECT, INSERT to payments_app_user. No UPDATE or DELETE.
Trigger: trg_direct_debit_events_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row().
payments.direct_debit_presentations (mutable — V003)¶
One row per debit-file entry received from the external provider. Links mandate validation outcome to the posting chain.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| presentation_id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| mandate_id | uuid | REFERENCES payments.direct_debit_mandates(mandate_id) | Null if mandate lookup failed (MANDATE_MISSING return) |
| account_id | uuid | NOT NULL | Soft ref — no FK |
| party_id | uuid | NOT NULL | |
| jurisdiction | text | NOT NULL CHECK (jurisdiction IN ('NZ','AU')) | |
| biller_id | text | NOT NULL | |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | text | NOT NULL CHECK (currency IN ('NZD','AUD')) | |
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','AUTHORISED','SETTLED','RETURNED','DISHONOURED')) | |
| return_code | text | Scheme return code (DDR/BECS) — populated on RETURNED or DISHONOURED | |
| return_reason | text | Plain-language mapped from return_code via reason-mapper | |
| posting_id | text | Application-layer ref to SD01 posting — no FK (cross-DB) | |
| dishonour_fee_posting_id | text | Application-layer ref to MOD-110 fee posting | |
| idempotency_key | text | NOT NULL UNIQUE | Provider file row identifier |
| trace_id | text | NOT NULL | |
| presented_at | timestamptz | NOT NULL DEFAULT now() | |
| settled_at | timestamptz | ||
| returned_at | timestamptz |
Grants: SELECT, INSERT, UPDATE to payments_app_user. No DELETE grant.
BPAY inbound tables (MOD-136)¶
AU-only. Handles inbound BPAY — business-banking customers registered as BPAY billers receiving bill payments from payers via the sponsor bank. Distinct from payments.bpay_payments + payments.bpay_biller_cache (MOD-119 outbound).
payments.bpay_inbound_billers (mutable — V001)¶
Per-tenant biller registrations. One row per business customer registered as a BPAY biller via the sponsor bank. Status is PENDING_REGISTRATION until the sponsor assigns a BPAY code (typically 3–5 business days). Table name uses inbound_billers prefix to distinguish from MOD-119's bpay_biller_cache (j-2 ruling).
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| party_id | uuid | NOT NULL | Soft ref — no FK (cross-domain) |
| account_id | uuid | NOT NULL | Soft ref — no FK (cross-domain) |
| bpay_code | text | UNIQUE | NULL until ACTIVE; assigned by sponsor on registration confirmation |
| biller_name | text | NOT NULL | |
| crn_format | text | NOT NULL CHECK (crn_format IN ('LUHN','REGEX','FIXED_LENGTH','NONE')) | Uppercase per SD04 enum standard |
| crn_regex_pattern | text | Required when crn_format = 'REGEX' |
|
| crn_fixed_length | int | Required when crn_format = 'FIXED_LENGTH' |
|
| status | text | NOT NULL DEFAULT 'PENDING_REGISTRATION' CHECK (status IN ('PENDING_REGISTRATION','ACTIVE','SUSPENDED','CANCELLED')) | |
| sponsor_submission_ref | text | Reference returned when registration submitted to sponsor | |
| jurisdiction | text | NOT NULL DEFAULT 'AU' CHECK (jurisdiction = 'AU') | AU-only module |
| registered_by | uuid | NOT NULL | Back-office staff user_id |
| activated_at | timestamptz | Set when sponsor confirmation received and staff PATCH applied | |
| idempotency_key | text | NOT NULL UNIQUE | |
| trace_id | text | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL DEFAULT now() |
Indexes: (party_id), (bpay_code) WHERE bpay_code IS NOT NULL, (status).
Grants: SELECT, INSERT, UPDATE to payments_app_user. No DELETE.
State machine: PENDING_REGISTRATION → ACTIVE (staff PATCH on sponsor confirmation; dev/uat admin endpoint advances deterministically — j-10 ruling). ACTIVE → SUSPENDED or CANCELLED by staff. No inbound credits while status is not ACTIVE (FR-605).
payments.bpay_inbound_payments (mutable — V002)¶
One row per item in the sponsor's daily BPAY inbound settlement file. Replay-safe: UNIQUE (settlement_batch_reference, scheme_item_id). Idempotency key provides defence-in-depth for Lambda retry.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| biller_id | uuid | NOT NULL REFERENCES payments.bpay_inbound_billers(id) | |
| bpay_code | text | NOT NULL | Denormalised from biller record for query performance |
| crn | text | NOT NULL | Customer reference number as received from payer's bank |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL DEFAULT 'AUD' | BPAY is AU-only |
| payer_bsb | text | NOT NULL | |
| payer_account_number | text | NOT NULL | |
| settlement_date | date | NOT NULL | |
| settlement_batch_reference | text | NOT NULL | Sponsor's batch reference |
| scheme_item_id | text | NOT NULL | Per-item identifier from settlement file |
| status | text | NOT NULL DEFAULT 'RECEIVED' CHECK (status IN ('RECEIVED','CRN_VALID','CRN_INVALID','POSTING_FAILED','POSTED','RETURNED')) | |
| crn_validation_result | text | PASS or FAIL:{reason} |
|
| posting_id | uuid | Application-layer ref to SD01 posting — no FK (cross-DB); populated after MOD-001 credit | |
| payment_id | uuid | FK to payments.payments synthetic row per j-3 ruling | |
| return_reason_code | text | Scheme return reason code; populated when status = 'RETURNED' |
|
| reconciliation_status | text | CHECK (reconciliation_status IN ('MATCHED','UNMATCHED','ESCALATED')) | Set by reconciliation.ts (FR-608) |
| reconciled_at | timestamptz | ||
| idempotency_key | text | NOT NULL UNIQUE | |
| trace_id | text | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL DEFAULT now() | |
| UNIQUE | (settlement_batch_reference, scheme_item_id) | Replay defence |
Indexes: (biller_id), (status), (settlement_date).
Grants: SELECT, INSERT, UPDATE to payments_app_user. No DELETE.
payments.bpay_inbound_events (immutable — V003, ADR-048 Cat 1)¶
Append-only audit log for all biller registration lifecycle and inbound payment state transitions. Satisfies PAY-002 LOG. Immutability enforced by trg_bpay_inbound_events_immutable (BEFORE UPDATE OR DELETE trigger calling fn_immutable_row()). Mirrors the payments.direct_debit_events pattern.
| Column | Type | Constraints | Notes |
|---|---|---|---|
| event_id | uuid | PRIMARY KEY DEFAULT gen_random_uuid() | |
| entity_type | text | NOT NULL CHECK (entity_type IN ('BILLER','PAYMENT')) | |
| entity_id | uuid | NOT NULL | biller_id or payment_id depending on entity_type |
| event_type | text | NOT NULL CHECK (event_type IN ('BILLER_REGISTRATION_SUBMITTED','BILLER_ACTIVATED','BILLER_SUSPENDED','BILLER_CANCELLED','PAYMENT_RECEIVED','CRN_VALIDATED','CRN_REJECTED','CREDIT_POSTED','NOTIFICATION_SENT','PAYMENT_RETURNED','RECONCILIATION_MATCHED','RECONCILIATION_UNMATCHED','RECONCILIATION_ESCALATED')) | |
| actor_type | text | NOT NULL CHECK (actor_type IN ('STAFF','SYSTEM','SPONSOR')) | |
| actor_id | uuid | Back-office staff user_id or service identity; null for SPONSOR events |
|
| detail | jsonb | NOT NULL DEFAULT '{}' | Event-specific payload |
| trace_id | text | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() |
Grants: SELECT, INSERT to payments_app_user. No UPDATE or DELETE.
Trigger: trg_bpay_inbound_events_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row().
Indexes: (entity_id), (created_at).
Batch payment tables (MOD-135)¶
Three tables owned by MOD-135. Flyway migrations V001–V003. Both jurisdictions (NZ + AU).
payments.batch_files (mutable — V001)¶
One row per uploaded batch file. Lifecycle: UPLOADED → VALIDATING → PENDING_APPROVAL → REJECTED | PROCESSING → SETTLED | FAILED.
| 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) — no FK |
| account_id | uuid | NOT NULL | Cross-domain ref to SD01 accounts.accounts(id) — no FK (source debit account) |
| file_format | text | NOT NULL CHECK (file_format IN ('ABA','CSV')) | |
| jurisdiction | text | NOT NULL CHECK (jurisdiction IN ('NZ','AU')) | |
| file_key | text | NOT NULL | S3 object key for the uploaded file |
| file_size_bytes | integer | NOT NULL CHECK (file_size_bytes > 0) | |
| declared_item_count | integer | NOT NULL CHECK (declared_item_count > 0) | Declared by client at upload |
| declared_total_amount | numeric(18,2) | NOT NULL CHECK (declared_total_amount > 0) | |
| validated_item_count | integer | Set after validation passes | |
| validated_total_amount | numeric(18,2) | Set after validation passes | |
| shortfall_amount | numeric(18,2) | FR-602 — positive if MOD-020 balance check fails | |
| status | text | NOT NULL DEFAULT 'UPLOADED' CHECK (status IN ('UPLOADED','VALIDATING','PENDING_APPROVAL','REJECTED','PROCESSING','SETTLED','FAILED')) | |
| rejection_reason | text | Structured error summary if REJECTED | |
| idempotency_key | text | NOT NULL UNIQUE | Caller-managed UUID per upload attempt (j-11) |
| confirmed_at | timestamptz | Customer confirmation timestamp | |
| settled_at | timestamptz | ||
| failed_at | timestamptz | ||
| trace_id | text | NOT NULL | OTel trace ID |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL |
Indexes:
- idx_batch_files_party_status on (party_id, status) WHERE status NOT IN ('SETTLED','FAILED','REJECTED')
- idx_batch_files_account_created on (account_id, created_at DESC)
GRANTs: SELECT, INSERT, UPDATE to bank_payments_lambda_role. No DELETE to any role.
payments.batch_items (mutable — V002)¶
One row per payment item within a batch. Status: PENDING → SUBMITTING → SUBMITTED → SETTLED | QUARANTINED | FAILED | RETURNED.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| batch_id | uuid | NOT NULL REFERENCES payments.batch_files(id) | |
| sequence_number | integer | NOT NULL | 1-based row number in the source file |
| payment_id | uuid | NOT NULL | Minted by MOD-135 at INSERT; passed to MOD-020 and MOD-001 per-item. No FK — payments.payments row created by MOD-020 in same call chain |
| beneficiary_account | text | NOT NULL | AU: BSB+account; NZ: 16-digit format |
| beneficiary_name | text | NOT NULL | |
| amount | numeric(18,2) | NOT NULL CHECK (amount > 0) | |
| currency | char(3) | NOT NULL | ISO-4217 |
| reference | text | Visible to beneficiary (≤ 12 chars) | |
| particulars | text | NZ only; ignored for AU (≤ 12 chars) | |
| posting_id | uuid | Soft ref to SD01 accounts.postings(id) — no FK; populated on SETTLED |
|
| return_posting_id | uuid | Soft ref — re-credit posting to source account for RETURNED items (FR-604) | |
| mod_020_decision | text | CHECK (mod_020_decision IN ('AUTHORISED','VALIDATION_FAILED','PENDING_AUTH')) | From per-item MOD-020 call |
| mod_020_failure_reason | text | ||
| status | text | NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','SUBMITTING','SUBMITTED','SETTLED','QUARANTINED','FAILED','RETURNED')) | |
| quarantine_reason | text | Populated when QUARANTINED (FR-603) | |
| failure_reason | text | Populated when FAILED | |
| returned_at | timestamptz | ||
| trace_id | text | NOT NULL | |
| created_at | timestamptz | NOT NULL DEFAULT now() | |
| updated_at | timestamptz | NOT NULL | |
| UNIQUE | (batch_id, sequence_number) | Replay-safe per file position |
Indexes:
- idx_batch_items_batch_status on (batch_id, status)
- idx_batch_items_payment_id on (payment_id)
Notes: payment_id is minted by MOD-135 before the per-item MOD-020 call — established pattern (same as MOD-141/MOD-119/MOD-120/MOD-122). No FK to payments.payments (cross-DB boundary: bank_payments vs bank_payments schema — MOD-020 creates the payments row in the same synchronous chain). posting_id and return_posting_id are application-layer soft refs (SD01 cross-DB).
GRANTs: SELECT, INSERT, UPDATE to bank_payments_lambda_role. No DELETE to any role.
payments.batch_events (immutable — V003, ADR-048 Cat 1)¶
Append-only audit log for all batch file and item state transitions. Satisfies PAY-002 LOG + NFR-024.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | uuid | PK DEFAULT gen_random_uuid() | |
| batch_id | uuid | NOT NULL REFERENCES payments.batch_files(id) | |
| item_id | uuid | REFERENCES payments.batch_items(id) | NULL for batch-level events |
| entity_type | text | NOT NULL CHECK (entity_type IN ('BATCH','ITEM')) | |
| event_type | text | NOT NULL CHECK (event_type IN ('BATCH_UPLOADED','BATCH_VALIDATING','BATCH_VALIDATED','BATCH_REJECTED','BATCH_CONFIRMED','BATCH_PROCESSING','BATCH_SETTLED','BATCH_FAILED','ITEM_PENDING','ITEM_SUBMITTING','ITEM_SUBMITTED','ITEM_SETTLED','ITEM_QUARANTINED','ITEM_FAILED','ITEM_RETURNED')) | |
| payload | jsonb | NOT NULL DEFAULT '{}' | Event-specific metadata |
| trace_id | text | NOT NULL | |
| event_timestamp | timestamptz | NOT NULL DEFAULT now() |
Immutability: trg_batch_events_immutable — BEFORE UPDATE OR DELETE OR TRUNCATE, calls fn_immutable_row(). PAY-002 LOG + NFR-024; 7-year retention.
Indexes:
- idx_batch_events_batch_id on (batch_id, event_timestamp DESC)
- idx_batch_events_item_id on (item_id) WHERE item_id IS NOT NULL
GRANTs: SELECT, INSERT only to bank_payments_lambda_role. No UPDATE, DELETE, or TRUNCATE to any role.
Agency banking tables (MOD-137)¶
payments.agency_batch_files (mutable — V001)¶
CREATE TABLE payments.agency_batch_files (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
agency_network text NOT NULL CHECK (agency_network IN ('AUSTRALIA_POST','NZ_POST','OTHER')),
file_reference text NOT NULL UNIQUE, -- idempotency: network-assigned file ID
file_key text NOT NULL, -- S3 key under agency-files/transactions/
settlement_file_key text, -- S3 key under agency-files/settlements/; NULL until received
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
batch_date date NOT NULL, -- transaction date from file header
declared_item_count int NOT NULL CHECK (declared_item_count > 0),
declared_total_deposit_amount numeric(18,2),
declared_total_withdrawal_amount numeric(18,2),
processed_item_count int,
posted_item_count int,
quarantined_item_count int,
status text NOT NULL DEFAULT 'RECEIVED'
CHECK (status IN ('RECEIVED','PROCESSING','SETTLED','RECONCILED','EXCEPTIONS')),
trace_id text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
settled_at timestamptz,
reconciled_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_agency_batch_files_status ON payments.agency_batch_files (status, received_at DESC);
CREATE INDEX idx_agency_batch_files_network ON payments.agency_batch_files (agency_network, batch_date DESC);
GRANTs: SELECT, INSERT, UPDATE to bank_payments_lambda_role. No DELETE to any role.
payments.agency_transactions (mutable — V002)¶
CREATE TABLE payments.agency_transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
batch_id uuid NOT NULL REFERENCES payments.agency_batch_files(id),
sequence_number int NOT NULL,
transaction_type text NOT NULL CHECK (transaction_type IN ('DEPOSIT','WITHDRAWAL','BALANCE_ENQUIRY')),
bsb text NOT NULL,
account_number text NOT NULL,
account_id uuid, -- resolved at MATCHED; no FK — cross-domain (core.accounts)
amount numeric(18,2) NOT NULL CHECK (amount > 0),
currency char(3) NOT NULL,
transaction_date date NOT NULL,
terminal_id text NOT NULL, -- FR-611 location metadata
agent_outlet_code text NOT NULL, -- FR-611 location metadata
posting_id uuid, -- soft ref SD01 accounts.postings(id); NULL until POSTED
aml_threshold_flagged boolean NOT NULL DEFAULT false,
mod_001_result text CHECK (mod_001_result IN ('POSTED','FAILED')),
status text NOT NULL DEFAULT 'UNMATCHED'
CHECK (status IN ('UNMATCHED','MATCHED','POSTED','QUARANTINED')),
quarantine_reason text,
failure_reason text,
trace_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (batch_id, sequence_number)
);
CREATE INDEX idx_agency_transactions_batch ON payments.agency_transactions (batch_id, status);
CREATE INDEX idx_agency_transactions_account ON payments.agency_transactions (account_id) WHERE account_id IS NOT NULL;
CREATE INDEX idx_agency_transactions_aml ON payments.agency_transactions (aml_threshold_flagged, transaction_date)
WHERE aml_threshold_flagged = true;
GRANTs: SELECT, INSERT, UPDATE to bank_payments_lambda_role. No DELETE to any role.
payments.agency_batch_events (immutable — V003, ADR-048 Cat 1)¶
CREATE TABLE payments.agency_batch_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
batch_id uuid NOT NULL REFERENCES payments.agency_batch_files(id),
transaction_id uuid REFERENCES payments.agency_transactions(id), -- NULL for BATCH-level events
entity_type text NOT NULL CHECK (entity_type IN ('BATCH','TRANSACTION')),
event_type text NOT NULL CHECK (event_type IN (
'BATCH_RECEIVED','BATCH_PROCESSING','BATCH_SETTLED',
'BATCH_RECONCILED','BATCH_EXCEPTIONS',
'TRANSACTION_MATCHED','TRANSACTION_POSTED',
'TRANSACTION_QUARANTINED','TRANSACTION_AML_FLAGGED'
)),
payload jsonb NOT NULL,
trace_id text NOT NULL,
event_timestamp timestamptz NOT NULL DEFAULT now()
);
-- ADR-048 Cat 1: no mutation after insert
CREATE TRIGGER trg_agency_batch_events_immutable
BEFORE UPDATE OR DELETE OR TRUNCATE ON payments.agency_batch_events
FOR EACH STATEMENT EXECUTE FUNCTION fn_immutable_row();
CREATE INDEX idx_agency_batch_events_batch ON payments.agency_batch_events (batch_id, event_timestamp DESC);
CREATE INDEX idx_agency_batch_events_tx ON payments.agency_batch_events (transaction_id)
WHERE transaction_id IS NOT NULL;
Immutability: trg_agency_batch_events_immutable — BEFORE UPDATE OR DELETE OR TRUNCATE, calls fn_immutable_row(). PAY-002 LOG + NFR-024; 7-year retention.
GRANTs: SELECT, INSERT only to bank_payments_lambda_role. No UPDATE, DELETE, or TRUNCATE to any role.
Schema: payments (continued) — MOD-124 physical card tables¶
payments.physical_cards¶
Mutable current-state record for each physical card issued. One row per card (not per customer). ADR-058: card_number_last4 only — full PANs live in the processor's PCI vault, accessed by opaque processor_card_ref.
CREATE TABLE payments.physical_cards (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
party_id uuid NOT NULL, -- soft ref to SD02 party.parties(party_id) — no FK (cross-domain)
account_id uuid NOT NULL REFERENCES accounts.accounts(id),
card_number_last4 char(4) NOT NULL, -- ADR-058: no full PAN in Neon
card_scheme text NOT NULL CHECK (card_scheme IN ('VISA','MASTERCARD')),
card_product_type text NOT NULL CHECK (card_product_type IN ('DEBIT','CREDIT')),
expiry_month smallint NOT NULL CHECK (expiry_month BETWEEN 1 AND 12),
expiry_year smallint NOT NULL,
embossed_name text NOT NULL,
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
sponsor_bin text NOT NULL, -- from SSM /bank/{stage}/mod-124/sponsor-bin-range/{nz|au}
status text NOT NULL DEFAULT 'ORDERED' CHECK (status IN (
'ORDERED','DISPATCHED','ACTIVE','FROZEN',
'CANCELLED','REPLACED','RENEWAL_PENDING')),
freeze_reason text CHECK (freeze_reason IN ('LOST','STOLEN','CUSTOMER_REQUEST','SYSTEM')),
processor_card_ref text, -- opaque reference in processor PCI vault
bureau_order_ref text, -- bureau's order tracking reference
bureau_dispatched_at timestamptz,
activated_at timestamptz,
cancelled_at timestamptz,
replacement_card_id uuid REFERENCES payments.physical_cards(id), -- populated when status = REPLACED
renewed_card_id uuid REFERENCES payments.physical_cards(id), -- populated when renewal card ordered
-- credit-ready columns — all NULL for debit cards in v1; used by future credit card path
credit_limit numeric(18,2),
credit_utilisation_pct numeric(5,2),
billing_cycle_day smallint CHECK (billing_cycle_day BETWEEN 1 AND 28),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_physical_cards_party_id ON payments.physical_cards (party_id);
CREATE INDEX idx_physical_cards_account_id ON payments.physical_cards (account_id);
CREATE INDEX idx_physical_cards_status ON payments.physical_cards (status)
WHERE status NOT IN ('CANCELLED','REPLACED');
CREATE INDEX idx_physical_cards_expiry ON payments.physical_cards (expiry_year, expiry_month)
WHERE status = 'ACTIVE'; -- 60-day pre-expiry scan (MOD-124 EventBridge Scheduler)
Owner: MOD-124. GRANTs: SELECT, INSERT, UPDATE to bank_payments_lambda_role. No DELETE or TRUNCATE to any role. Deletions are logical (status = CANCELLED).
payments.card_events¶
Immutable append-only audit log of every card lifecycle state transition. ADR-048 Cat 1. Provides the complete audit trail required by PAY-006 and DT-001.
CREATE TABLE payments.card_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
card_id uuid NOT NULL REFERENCES payments.physical_cards(id),
event_type text NOT NULL CHECK (event_type IN (
'ORDERED','DISPATCHED',
'ACTIVATION_REQUESTED','ACTIVATED',
'FREEZE_REQUESTED','FROZEN',
'UNFREEZE_REQUESTED','UNFROZEN',
'CANCELLATION_REQUESTED','CANCELLED',
'REPLACEMENT_ORDERED',
'PIN_CHANGE_REQUESTED','PIN_CHANGED',
'RENEWAL_ORDERED')),
source_module text NOT NULL,
actor text, -- customer party_id, staff ID, or 'SYSTEM'
channel text NOT NULL DEFAULT 'SYSTEM'
CHECK (channel IN ('APP','API','SYSTEM','BUREAU_WEBHOOK')),
details jsonb NOT NULL DEFAULT '{}',
trace_id uuid,
idempotency_key text NOT NULL,
event_timestamp timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now()
);
-- ADR-048 Cat 1: no mutation after insert
CREATE TRIGGER trg_card_events_immutable
BEFORE UPDATE OR DELETE OR TRUNCATE ON payments.card_events
FOR EACH STATEMENT EXECUTE FUNCTION fn_immutable_row();
CREATE INDEX idx_card_events_card_id ON payments.card_events (card_id, event_timestamp DESC);
CREATE INDEX idx_card_events_event_type ON payments.card_events (event_type);
CREATE UNIQUE INDEX idx_card_events_idempotency ON payments.card_events (idempotency_key);
Immutability: trg_card_events_immutable — BEFORE UPDATE OR DELETE OR TRUNCATE, calls fn_immutable_row(). PAY-006 LOG + DT-001 GATE; 7-year retention per NFR-024.
GRANTs: SELECT, INSERT only to bank_payments_lambda_role. No UPDATE, DELETE, or TRUNCATE to any role.
Immutability triggers (SD04)¶
| Table | Trigger | Type |
|---|---|---|
payments.payment_events |
trg_payment_events_immutable |
ADR-048 Cat 1 |
payments.direct_debit_events |
trg_direct_debit_events_immutable |
ADR-048 Cat 1 |
payments.bpay_inbound_events |
trg_bpay_inbound_events_immutable |
ADR-048 Cat 1 |
payments.batch_events |
trg_batch_events_immutable |
ADR-048 Cat 1 |
payments.agency_batch_events |
trg_agency_batch_events_immutable |
ADR-048 Cat 1 |
payments.card_events |
trg_card_events_immutable |
ADR-048 Cat 1 |
Cross-domain references¶
| This table | Column | References | Notes |
|---|---|---|---|
| payments.payments | party_id | SD02 party.parties(party_id) |
Every payment is initiated by a verified party |
| payments.payments | from_account_id | SD01 accounts.accounts(id) |
Debit account must exist in core banking |
| payments.payments | to_account_id | SD01 accounts.accounts(id) |
Internal credit account; NULL for external payments |
| payments.payments | sanctions_result_id | SD02 kyc.sanctions_results(id) |
Payment-level sanctions screen |
| payments.payment_limits | party_id | SD02 party.parties(party_id) |
Limits are per-party |
| payments.fx_locks | party_id | SD02 party.parties(party_id) |
|
| payments.ifti_cmir_queue | submission_id | SD03 aml.regulatory_submissions(id) |
Soft reference only — no FK constraint; back-populated by MOD-019 asynchronously |
| payments.open_banking_consents | party_id | SD02 party.parties(party_id) |
|
| payments.payid_registrations | party_id | SD02 party.parties(party_id) |
Every PayID registration is associated with a verified party |
| payments.bpay_inbound_billers | party_id | SD02 party.parties(party_id) |
Soft ref — no FK (cross-domain) |
| payments.bpay_inbound_billers | account_id | SD01 accounts.accounts(id) |
Soft ref — no FK (cross-domain) |
| payments.bpay_inbound_payments | payment_id | payments.payments(id) |
Synthetic payments.payments row per j-3 ruling; application-layer only |
| payments.batch_files | party_id | SD02 party.parties(party_id) |
Soft ref — no FK (cross-domain) |
| payments.batch_files | account_id | SD01 accounts.accounts(id) |
Soft ref — no FK (cross-domain); source debit account |
| payments.batch_items | payment_id | payments.payments(id) |
Minted by MOD-135; payments.payments row created by MOD-020 in same chain — application-layer only |
| payments.batch_items | posting_id | SD01 accounts.postings(id) |
Soft ref — no FK; populated on SETTLED |
| payments.batch_items | return_posting_id | SD01 accounts.postings(id) |
Soft ref — re-credit posting for RETURNED items (FR-604) |
| payments.agency_batch_files | (no cross-domain cols) | — | Network-scoped; account references live on agency_transactions |
| payments.agency_transactions | account_id | SD01 core.accounts(id) |
Soft ref — no FK (cross-domain); resolved at MATCHED via Neon read |
| payments.agency_transactions | posting_id | SD01 accounts.postings(id) |
Soft ref — no FK; populated on POSTED |
| payments.physical_cards | party_id | SD02 party.parties(party_id) |
Soft ref — no FK (cross-domain); every card is issued to a verified party |
| payments.physical_cards | account_id | SD01 accounts.accounts(id) |
Card is linked to a deposit account |
Consumers of SD04 data: SD01 MOD-001 creates postings from settled payment records. SD03 MOD-016/017 read payments transaction data for AML typology analysis via CDC. SD06 Snowflake receives all tables via CDC (MOD-042) for fraud analytics and capital calculations. SD08 MOD-071 initiates payments via the pre-validation API and reads payment history via MOD-070. SD08 MOD-072 manages PayID registrations via the MOD-120 API. The ifti_cmir_queue is consumed by SD03 MOD-019.