Skip to content

SD05 — Credit data model

Database: credit (Neon Postgres, ap-southeast-2) Repo: bank-credit 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: SD05 Credit Decisioning & Loan Platform

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

The credit schema is the primary write domain for MOD-027 (affordability calculator), MOD-028 (credit score and risk rating), MOD-029 (pre-approval engine), MOD-030 (IFRS 9 stage allocation), MOD-031 (ECL calculation and GL posting), MOD-059 (credit bureau submission), MOD-065 (credit servicing and collections), and MOD-066 (collateral and security management). Credit decisions are immutable once recorded. Stage allocations are updated monthly or on deterioration triggers, with each change creating a new allocation row. ECL provisions are posted to the GL via SD01.


Schema: credit

credit.credit_applications

One row per credit application submitted by a customer. Covers personal loans, credit lines, overdraft facilities, and mortgages. The application progresses through decisioning stages before a final credit decision is recorded in credit_decisions.

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)
application_reference text NOT NULL UNIQUE Human-readable reference (e.g. APP-2026-084321)
product_type text NOT NULL CHECK (product_type IN ('PERSONAL_LOAN','CREDIT_LINE','OVERDRAFT','MORTGAGE','BUSINESS_LOAN','FLEXIBLE_FACILITY')) Extended from 5 → 6 values by MOD-162 V001 migration
requested_amount numeric(18,2) NOT NULL
requested_currency char(3) NOT NULL
requested_term_months int Loan term; NULL for revolving facilities
purpose text NOT NULL Declared purpose of credit
application_status text NOT NULL CHECK (application_status IN ('DRAFT','SUBMITTED','UNDER_ASSESSMENT','PRE_APPROVED','APPROVED','CONDITIONALLY_APPROVED','DECLINED','WITHDRAWN','EXPIRED'))
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
submitted_at timestamptz NULL while in DRAFT
decision_at timestamptz When a final decision was recorded
channel text NOT NULL CHECK (channel IN ('APP','BRANCH','API','PRE_APPROVAL'))
cdd_tier_at_application text NOT NULL Customer's CDD tier at application time
credit_score_at_application int Credit bureau score at application
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL
deleted_at timestamptz

Indexes: - idx_credit_applications_party_id on (party_id) - idx_credit_applications_status on (application_status) WHERE application_status NOT IN ('DECLINED','WITHDRAWN','EXPIRED') - idx_credit_applications_submitted_at on (submitted_at DESC) WHERE submitted_at IS NOT NULL - idx_credit_applications_reference on (application_reference)


credit.credit_decisions

Immutable record of each credit decision event. One application may have multiple decisions (e.g. initial decline, reconsideration approval). The most recent decision with is_current = true is the operative one.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
application_id uuid NOT NULL REFERENCES credit.credit_applications(id)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
decision_type text NOT NULL CHECK (decision_type IN ('APPROVE','CONDITIONALLY_APPROVE','DECLINE','REFER','PRE_APPROVE','COUNTER_OFFER'))
decision_source text NOT NULL CHECK (decision_source IN ('AUTO','MANUAL','PRE_APPROVAL_ENGINE','RECONSIDERATION'))
approved_amount numeric(18,2) NULL for declines
approved_currency char(3)
approved_term_months int
interest_rate numeric(8,6) Approved rate as decimal
conditions jsonb NOT NULL DEFAULT '[]' Any conditions attached (e.g. document requirements)
decline_reasons jsonb NOT NULL DEFAULT '[]' Reason codes for declined applications
risk_rating text NOT NULL CHECK (risk_rating IN ('A1','A2','B1','B2','C1','C2','D','E')) Internal credit risk grade
credit_score int Score used in decision
affordability_assessment_id uuid REFERENCES credit.affordability_assessments(id)
policy_refs jsonb NOT NULL DEFAULT '["CRE-002","CRE-003"]'
decided_at timestamptz NOT NULL DEFAULT now()
decided_by text NOT NULL Module or staff ID
is_current boolean NOT NULL DEFAULT true Only the operative decision is true
superseded_by uuid REFERENCES credit.credit_decisions(id)
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_credit_decisions_application_id on (application_id) - idx_credit_decisions_party_current on (party_id) WHERE is_current = true - idx_credit_decisions_decided_at on (decided_at DESC)

Notes: Rows are immutable once written — enforced by trg_credit_decisions_immutable (ADR-048 Category 1). Reconsiderations and counter-offers create new rows; the superseded decision retains its original state.


credit.affordability_assessments

Results of the affordability calculation run by MOD-027. One row per assessment run. Multiple assessments may exist for a single application (e.g. if the customer updates their income information).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
application_id uuid REFERENCES credit.credit_applications(id) Nullable today. FK added by MOD-029 V002. NOT NULL deferred to a follow-on MOD-029 V003 once MOD-027 historical NULL rows from dev/test runs are backfilled or aged out of retention.
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
gross_income numeric(18,2) NOT NULL Declared gross annual income
income_currency char(3) NOT NULL
income_verification_method text NOT NULL CHECK (income_verification_method IN ('PAYSLIP','TAX_RETURN','BANK_STATEMENT','OPEN_BANKING','DECLARED'))
net_income numeric(18,2) NOT NULL Post-tax, post-obligations income
hem_benchmark numeric(18,2) NOT NULL Household Expenditure Measure benchmark applied
declared_expenses numeric(18,2) NOT NULL DEFAULT 0.00 Customer-declared monthly expenses
assessed_expenses numeric(18,2) NOT NULL Higher of declared and HEM
existing_debt_commitments numeric(18,2) NOT NULL DEFAULT 0.00 Existing monthly debt repayments
net_disposable_income numeric(18,2) NOT NULL net_income - assessed_expenses - existing_debt_commitments
proposed_repayment numeric(18,2) NOT NULL Monthly repayment for the requested loan
ndi_after_repayment numeric(18,2) NOT NULL net_disposable_income - proposed_repayment
dti_ratio numeric(8,4) NOT NULL Debt-to-income ratio at assessment time (FR-171)
stress_rate_applied numeric(8,6) NOT NULL CHECK (stress_rate_applied >= 0) Actual rate used in serviceability calc — self-contained audit, not reconstructable from AppConfig history (CRE-002)
buffer_applied_bps int NOT NULL CHECK (buffer_applied_bps >= 0) Buffer applied in basis points (e.g. 300 = 3.00% for AU NCCP) — self-contained audit
regulatory_framework text NOT NULL CHECK (regulatory_framework IN ('CCCFA','NCCP')) Jurisdiction-derived framework label
proposed_repayment_total_interest numeric(18,2) NOT NULL CHECK (proposed_repayment_total_interest >= 0) CON-004 LOG — total interest payable over loan term; captured for caller MOD-029 to pass to MOD-050 disclosure
proposed_repayment_total_cost numeric(18,2) NOT NULL CHECK (proposed_repayment_total_cost >= 0) CON-004 LOG — total cost of credit; captured for caller MOD-029 to pass to MOD-050 disclosure
dti_threshold numeric(8,4) NOT NULL CHECK (dti_threshold >= 0) Active DTI threshold used (per product_type × jurisdiction config) — self-contained audit
affordability_result text NOT NULL CHECK (affordability_result IN ('PASS','MARGINAL','FAIL'))
decline_reason_codes jsonb NOT NULL DEFAULT '[]' Populated when affordability_result = 'FAIL'
hem_source_version text NOT NULL HEM table version used
policy_refs jsonb NOT NULL DEFAULT '["CRE-002","CRE-003","CON-004","REP-005"]' Policy codes satisfied by this row
assessed_at timestamptz NOT NULL DEFAULT now()
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_affordability_application_id on (application_id) WHERE application_id IS NOT NULL - idx_affordability_party_id on (party_id) - idx_affordability_assessed_at on (assessed_at DESC)

Notes: Rows are immutable after insertion — enforced by trg_affordability_assessments_immutable (ADR-048 Category 1), which reuses credit.fn_immutable_row() provisioned by MOD-128 V001. Updated income information results in a new assessment row rather than an update to the existing one.


credit.credit_scores

Composite credit score and internal risk rating produced by MOD-028 for each scoring run. One row per score computation. Multiple rows may exist for the same party across different applications or periodic review runs. Append-only — score history is never overwritten.

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)
application_id uuid REFERENCES credit.credit_applications(id) Nullable today. FK added by MOD-029 V002. NOT NULL deferred to a follow-on MOD-029 V003 once MOD-028 historical NULL rows from dev/test runs are backfilled or aged out of retention.
affordability_assessment_id uuid REFERENCES credit.affordability_assessments(id) Source affordability assessment used as scoring input
bureau_enquiry_id uuid REFERENCES credit.bureau_enquiries(enquiry_id) NULL when bureau data unavailable and cdd_soft_fallback = true
behavioural_score int NOT NULL Composite internal score (0–1000 scale); derived from bureau, affordability, and CDD components
bureau_score int Bureau-supplied score at time of scoring; NULL if bureau unavailable
bureau_used text Bureau name when bureau_score is populated (e.g. EQUIFAX_AU, CENTRIX)
bureau_staleness_days int Days since bureau file date at time of scoring (AD-4 staleness audit)
bureau_stale_flag boolean NOT NULL DEFAULT false True when bureau_staleness_days > 30 (AD-4 threshold)
cdd_tier text NOT NULL CDD tier at time of scoring (from MOD-010)
cdd_soft_fallback boolean NOT NULL DEFAULT false AD-6: true when CDD tier unavailable and a fallback tier was applied — DT-005 deviation audit
risk_rating text NOT NULL CHECK (risk_rating IN ('A1','A2','B1','B2','C1','C2','D','E')) Internal credit risk grade; maps to credit_decisions.risk_rating
basel_risk_weight numeric(8,6) NOT NULL DEFAULT 0.750000 AD-3: Basel standardised retail risk weight (75% baseline); overridable by product config; read by MOD-033 RWA calculation
score_components jsonb NOT NULL DEFAULT '{}' Weighted component breakdown and input values for model audit (DT-005 model inventory)
model_version text NOT NULL Scorecard model version (CRE-003 governance log)
policy_refs jsonb NOT NULL DEFAULT '["CRE-001","CRE-003","DT-005","CLQ-001"]'
scored_at timestamptz NOT NULL DEFAULT now()
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_credit_scores_party_id on (party_id) - idx_credit_scores_application_id on (application_id) WHERE application_id IS NOT NULL - idx_credit_scores_scored_at on (scored_at DESC) - idx_credit_scores_risk_rating on (risk_rating)

Notes: Rows are immutable after insertion — enforced by trg_credit_scores_immutable (ADR-048 Category 1), which reuses credit.fn_immutable_row() provisioned by MOD-128 V001. Each scoring run always inserts a new row regardless of outcome.


credit.risk_scores_mirror

Append-only mirror of SD06 customer risk scores written by the MOD-028 mirror-writer Lambda, which consumes bank.risk-platform/customer_risk_score_updated cross-bus EventBridge events. Provides SD05 modules with local read access to SD06 risk scores without crossing the domain boundary at query time (ADR-038 hot-path constraint). One row per event received.

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)
source_event_id text NOT NULL UNIQUE EB event ID from bank.risk-platform/customer_risk_score_updated; UNIQUE constraint prevents duplicate event processing
sd06_risk_score int NOT NULL Numeric risk score as published by SD06
sd06_risk_tier text NOT NULL Tier label from SD06 model (e.g. LOW, MEDIUM, HIGH, VERY_HIGH)
model_version text NOT NULL SD06 model version at event time
score_date date NOT NULL Date the score was computed by SD06
payload jsonb NOT NULL DEFAULT '{}' Full EB event payload retained for audit
mirrored_at timestamptz NOT NULL DEFAULT now() When the mirror-writer Lambda consumed the event
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_risk_scores_mirror_party_id on (party_id) - idx_risk_scores_mirror_source_event_id on (source_event_id) — enforces UNIQUE, also lookup path for idempotency check - idx_risk_scores_mirror_score_date on (score_date DESC)

Notes: Rows are immutable after insertion — enforced by trg_risk_scores_mirror_immutable (ADR-048 Category 1). Mirror-writer Lambda uses source_event_id UNIQUE constraint as the idempotency gate; SQS DLQ captures failed writes for operational alerting (AD-8). BankCreditRole requires events:PutRule + events:PutTargets on the bank-risk-platform bus, provisioned by MOD-104 cross-bus grant handoff.


credit.hem_benchmarks (temporary stand-in)

Stand-in table — temporary. Provisioned by MOD-027 V001 as a local substitute until the dedicated SD06 HEM ingestion module ships and publishes a cross-domain risk.hem_benchmarks_readable view (AP-010 pattern 1). Once that view is available, MOD-027 will cut over its HemLookup adapter and a follow-up migration will drop this table.

Household Expenditure Measure benchmarks per (jurisdiction, household_type, income_band). MOD-027 uses the benchmark to substitute assessed_expenses when declared expenses fall below the regulatory floor (CRE-002, APRA RG 209.60).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
household_type text NOT NULL CHECK (household_type IN ('SINGLE_NO_DEPENDENTS','COUPLE_NO_DEPENDENTS','SINGLE_WITH_DEPENDENTS','COUPLE_WITH_DEPENDENTS'))
income_band_lower numeric(18,2) NOT NULL Annual gross income lower bound (AUD/NZD)
income_band_upper numeric(18,2) NULL = no upper bound
monthly_benchmark numeric(18,2) NOT NULL CHECK (monthly_benchmark > 0) Monthly HEM amount substituted into assessed_expenses when declared < benchmark
source text NOT NULL CHECK (source IN ('ABS','STATS_NZ','STAND_IN')) Data source
source_version text NOT NULL e.g. 2026-Q1 or STAND-IN-v1
effective_from date NOT NULL
effective_to date NULL = current row
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now()

Indexes: partial (jurisdiction, household_type, effective_from DESC) WHERE effective_to IS NULL for active-row lookup.

V002 seed: stage-gated dev seed (skipped on prod) provides AU + NZ values for all four household_types across two-to-three income bands with source='STAND_IN', source_version='STAND-IN-v1'.


credit.credit_bureau_requests

Outbound CCR submission log owned by MOD-059. One row per bureau submission attempt (SUBMISSION, UPDATE, or CORRECTION). Inbound bureau enquiries (soft pull / hard enquiry) are logged separately in credit.bureau_enquiries (MOD-128).

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)
application_id uuid REFERENCES credit.credit_applications(id) NULL for background checks
bureau text NOT NULL CHECK (bureau IN ('EQUIFAX','ILLION','CENTRIX','EXPERIAN'))
inquiry_type text NOT NULL CHECK (inquiry_type IN ('SUBMISSION','UPDATE','CORRECTION')) MOD-059 outbound types only. SOFT_PULL/HARD_ENQUIRY belong to MOD-128's credit.bureau_enquiries.
credit_score int Populated by MOD-128 inbound path only; NULL on MOD-059 outbound rows.
credit_file_payload jsonb NOT NULL DEFAULT '{}' Redacted bureau response; full payload in S3
s3_key text S3 object key for the full credit file (inbound path)
request_status text NOT NULL CHECK (request_status IN ('PENDING','SUCCESS','FAILED','TIMEOUT','HALTED')) HALTED = validation failure (FR-191 halt path; AD k-8)
requested_at timestamptz NOT NULL DEFAULT now()
responded_at timestamptz
policy_ref text NOT NULL DEFAULT 'REP-010'
submission_file_hash text SHA-256 of the assembled outbound file (FR-192)
bureau_ack_reference text Bureau-supplied acknowledgement reference (FR-192)
rejection_reason text Bureau-supplied rejection text (FR-192)
submitted_at timestamptz When the file was actually sent (distinct from requested_at)
s3_payload_key text S3 key of the assembled outbound file
submission_batch_id uuid REFERENCES credit.bureau_submission_batches(id) ON DELETE SET NULL Links to the monthly batch when applicable (FR-190 batch submissions)
trace_id uuid ADR-031 trace propagation
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_credit_bureau_party_id on (party_id) - idx_credit_bureau_application_id on (application_id) WHERE application_id IS NOT NULL - idx_credit_bureau_requested_at on (requested_at DESC)

Notes: Rows are immutable after insertion — enforced by trg_credit_bureau_requests_immutable (ADR-048 Category 1). Required for credit reporting compliance; bureau inquiry records must not be altered.


credit.bureau_submission_batches

Monthly FR-190 batch metadata. One row per (bureau, reporting_month, jurisdiction) — the batch header for the full account performance file sent to each bureau each month. Owned by MOD-059. Mutable (status transitions).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
bureau text NOT NULL CHECK (bureau IN ('CENTRIX','EQUIFAX_AU'))
reporting_month date NOT NULL CHECK (reporting_month = date_trunc('month', reporting_month)::date) First calendar day of the reporting month (e.g. 2026-05-01)
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
account_count int NOT NULL DEFAULT 0 CHECK (account_count >= 0) Number of accounts included in the batch
file_hash text SHA-256 of assembled file; NULL until BUILDING completes
file_s3_key text
status text NOT NULL DEFAULT 'BUILDING' CHECK (status IN ('BUILDING','VALIDATED','TRANSMITTED','REJECTED','HALTED'))
validation_outcome jsonb NOT NULL DEFAULT '{}' AJV error list when status=HALTED or REJECTED
bureau_ack_reference text
rejection_reason text
started_at timestamptz NOT NULL DEFAULT now()
completed_at timestamptz
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now()

Constraints: UNIQUE on (bureau, reporting_month, jurisdiction).

Indexes: idx_bureau_batches_status partial WHERE status IN ('BUILDING','VALIDATED','HALTED'); idx_bureau_batches_reporting_month on (reporting_month DESC).

Triggers: trg_bureau_submission_batches_touch_updated_at (touch trigger). Mutable table — no immutability trigger.


credit.bureau_disputes

Customer dispute log and CCCFA / Privacy Code dispute lifecycle. Mutable (status transitions). Owned by MOD-059.

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)
application_id uuid REFERENCES credit.credit_applications(id)
bureau text NOT NULL CHECK (bureau IN ('CENTRIX','EQUIFAX_AU'))
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
dispute_reference text NOT NULL UNIQUE Human-readable reference (e.g. DSP-2026-00123)
lodged_at timestamptz NOT NULL
lodged_by_channel text NOT NULL CHECK (lodged_by_channel IN ('APP','BRANCH','API','EMAIL'))
description text NOT NULL
status text NOT NULL DEFAULT 'LODGED' CHECK (status IN ('LODGED','INVESTIGATING','UPHELD','REJECTED','WITHDRAWN','RESOLVED'))
resolution_notes text
resolved_at timestamptz
correction_submission_id uuid REFERENCES credit.credit_bureau_requests(id) Auto-populated on UPHELD when a CORRECTION submission is generated
regulatory_deadline timestamptz NOT NULL Statutory resolution deadline calculated at lodge time (NZ: +28 calendar days per Privacy Act 2020; AU: +30 calendar days per Privacy Act 1988)
trace_id uuid
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now()

Indexes: (party_id, lodged_at DESC); (status) partial WHERE status IN ('LODGED','INVESTIGATING'); (regulatory_deadline ASC) partial WHERE status IN ('LODGED','INVESTIGATING').

Triggers: trg_bureau_disputes_touch_updated_at (touch trigger). Mutable — no immutability trigger.


credit.ecl_provisions

Monthly IFRS 9 Expected Credit Loss (ECL) provision records produced by MOD-031. One row per loan account per provision run. ECL amounts are posted to the general ledger via SD01 (MOD-001).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
provision_date date NOT NULL Month-end date of this provision run
ifrs9_stage int NOT NULL CHECK (ifrs9_stage IN (1,2,3)) IFRS 9 stage at provision date
outstanding_balance numeric(18,2) NOT NULL
pd_12m numeric(10,8) NOT NULL CHECK (pd_12m >= 0 AND pd_12m <= 1) 12-month probability of default
pd_lifetime numeric(10,8) NOT NULL CHECK (pd_lifetime >= 0 AND pd_lifetime <= 1) Lifetime probability of default
lgd numeric(10,8) NOT NULL CHECK (lgd >= 0 AND lgd <= 1) Loss given default
ead numeric(18,2) NOT NULL Exposure at default
ecl_12m numeric(18,2) NOT NULL 12-month ECL
ecl_lifetime numeric(18,2) NOT NULL Lifetime ECL
ecl_recognised numeric(18,2) NOT NULL ECL amount actually recognised (stage-dependent)
model_version text NOT NULL ECL model version used
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')) FR-186 portfolio split by jurisdiction
currency char(3) NOT NULL Needed for GL posting entries and outbound event
run_type text NOT NULL CHECK (run_type IN ('MONTHLY','DAILY_INCREMENTAL','EVENT_TRIGGERED')) Distinguishes the three ECL calculation paths
ecl_run_id uuid NOT NULL REFERENCES credit.ecl_runs(id) Links to the run-level audit record (FR-187)
previous_ecl_recognised numeric(18,2) NOT NULL DEFAULT 0.00 Prior recognised amount for the same loan — captures the delta for movement-zero skip logic
gl_posting_id uuid NULL — cross-domain ref to bank_core.accounts.postings(id); no FK enforced Posting in SD01 for this provision movement
calculated_at timestamptz NOT NULL DEFAULT now()
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_ecl_provisions_loan_account_date on (loan_account_id, provision_date DESC) - idx_ecl_provisions_provision_date on (provision_date) - idx_ecl_provisions_stage on (ifrs9_stage, provision_date)

Constraints: - uq_ecl_provisions_loan_date_runtype UNIQUE (loan_account_id, provision_date, run_type) — one row per loan per provision date per run type; a MONTHLY snapshot and an EVENT_TRIGGERED row may co-exist on the same date.

Notes: Rows are immutable after insertion — enforced by trg_ecl_provisions_immutable (ADR-048 Category 1). Each calculation path inserts a new row; prior ECL figures must remain unaltered for IFRS 9 audit purposes.


credit.ecl_runs (MOD-031)

Run-level audit record for each ECL calculation batch. One row per run invocation. Mutable during execution (status transitions); application-layer immutability enforced for terminal states (COMPLETE, FAILED) — no further updates permitted once the run reaches either terminal state. FR-187 7-year retention via Snowflake CDC.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid() = ecl_run_id referenced by ecl_provisions and outbound events
run_type text NOT NULL CHECK (run_type IN ('MONTHLY','DAILY_INCREMENTAL','EVENT_TRIGGERED')) Which calculation path triggered this run
provision_date date NOT NULL Last calendar day of the preceding month for MONTHLY; sweep/event date for other types
status text NOT NULL DEFAULT 'RUNNING' CHECK (status IN ('RUNNING','PENDING_APPROVAL','APPROVED','COMPLETE','FAILED')) State machine — RUNNING→PENDING_APPROVAL or COMPLETE; PENDING_APPROVAL→APPROVED→COMPLETE; any state→FAILED
trigger_loan_account_id uuid NULL REFERENCES credit.loan_accounts(id) Populated for EVENT_TRIGGERED runs only
loans_in_run int NOT NULL DEFAULT 0 Count of loan accounts processed
total_provision_movement numeric(18,2) NULL Signed net ECL movement across all loans in the run
previous_run_total numeric(18,2) NULL Total recognised ECL from the prior MONTHLY run — used by FR-188 materiality gate
materiality_breach boolean NOT NULL DEFAULT false FR-188: true when total_provision_movement exceeds the configured materiality threshold
approved_by_staff_id text NULL Staff ID of approver; 'SYSTEM_FIRST_RUN' for the first-ever monthly run (FR-188 first-run edge case)
approved_at timestamptz NULL
approval_committee_ref text NULL Governance ticket reference for audit
model_version text NOT NULL ECL model version — recorded on every run for FR-187 model audit
parameter_set_version text NOT NULL Combined PD+LGD parameter set version (e.g. pd-v1.0.0+lgd-v1.0.0)
trace_id uuid NOT NULL ADR-031 trace propagation
started_at timestamptz NOT NULL DEFAULT now()
completed_at timestamptz NULL Populated on COMPLETE or FAILED
failure_reason text NULL Populated on FAILED — human-readable failure description
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now() Touch trigger maintains updated_at

Indexes: - (run_type, provision_date DESC) — month-over-month comparisons and RWA engine queries - (status) WHERE status NOT IN ('COMPLETE','FAILED') — operational dashboard for in-flight runs - (provision_date DESC) — chronological audit queries

Notes: Mutable table — status transitions are legitimate writes. Application-layer immutability is enforced in the Lambda handler: once status is COMPLETE or FAILED, the row is not updated further. Not an ADR-048 Cat 1 trigger (status progression is a valid mutation); the terminal-state lock is enforced by the approve-ecl-run handler's state machine.


credit.ifrs9_stage_allocations (MOD-030)

Append-only IFRS 9 stage allocation log. One row per staging event — initial allocation, periodic reassessment, SICR trigger, or cure. The latest row per (loan_account_id, effective_date DESC, allocated_at DESC) is the current stage. MOD-031 reads this table to compute ECL. ADR-048 Cat 1 immutable.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
party_id uuid NOT NULL Cross-domain ref to SD02 party.parties(party_id)
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')) Denormalised so MOD-031 reads without a join (AD k-1)
ifrs9_stage smallint NOT NULL CHECK (ifrs9_stage IN (1,2,3))
previous_ifrs9_stage smallint NULL CHECK (previous_ifrs9_stage IN (1,2,3)) For transition logging
trigger_reason text NOT NULL CHECK (trigger_reason IN ('INITIAL_ALLOCATION','DPD_THRESHOLD','WATCHLIST_FLAG','PD_INCREASE','CREDIT_IMPAIRED','CURE_TO_STAGE_1','MANUAL_OVERRIDE','MONTHLY_REASSESSMENT'))
arrears_days_at_allocation int NOT NULL CHECK (arrears_days_at_allocation >= 0)
risk_rating text NULL CHECK (risk_rating IN ('A1','A2','B1','B2','C1','C2','D','E')) Sourced from credit.credit_scores
pd_origination numeric(8,6) NULL CHECK (pd_origination BETWEEN 0 AND 1)
pd_lifetime numeric(8,6) NULL CHECK (pd_lifetime BETWEEN 0 AND 1)
exposure_at_default numeric(18,2) NOT NULL CHECK (exposure_at_default >= 0)
loan_status_at_allocation text NOT NULL
effective_date date NOT NULL
allocated_at timestamptz NOT NULL DEFAULT now()
source_event_id uuid NULL Inbound EventBridge event_id
source text NOT NULL CHECK (source IN ('FACILITY_EVENT','DAILY_SWEEP','MANUAL_OVERRIDE','MONTHLY_REASSESSMENT'))
committee_approval_id text NULL FR-184 GATE: required for MANUAL_OVERRIDE (biconditional CHECK)
override_reason text NULL Populated on MANUAL_OVERRIDE
override_actor text NULL Populated on MANUAL_OVERRIDE
trace_id uuid NOT NULL ADR-031 propagation

Indexes: - (loan_account_id, effective_date DESC, allocated_at DESC) — current stage lookup - (ifrs9_stage, effective_date DESC) — portfolio reporting / capital engine - (party_id) — customer-360 view

Constraints: - chk_manual_override_governance (biconditional): trigger_reason = 'MANUAL_OVERRIDE' AND committee_approval_id IS NOT NULL AND override_actor IS NOT NULL OR trigger_reason <> 'MANUAL_OVERRIDE' AND committee_approval_id IS NULL

Immutability: trg_ifrs9_stage_allocations_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row(). ADR-048 Cat 1.

Grants: bank_credit_app_user: SELECT, INSERT (no UPDATE/DELETE). bank_credit_readonly: SELECT.


credit.loan_accounts

Operational loan account records for approved and active loans, maintained by MOD-065. One row per disbursed loan. Links to both the credit application (origination) and the SD01 account (ledger).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
application_id uuid NOT NULL REFERENCES credit.credit_applications(id)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
account_id uuid NOT NULL REFERENCES accounts.accounts(id) Linked SD01 account for fund disbursement and repayment
loan_reference text NOT NULL UNIQUE Human-readable loan reference
product_type text NOT NULL CHECK (product_type IN ('PERSONAL_LOAN','CREDIT_LINE','OVERDRAFT','MORTGAGE','BUSINESS_LOAN','FLEXIBLE_FACILITY')) Extended from 5 → 6 values by MOD-162 V001 migration
principal_amount numeric(18,2) NOT NULL Original disbursed principal
outstanding_principal numeric(18,2) NOT NULL DEFAULT 0.00 Current outstanding principal
currency char(3) NOT NULL
interest_rate numeric(8,6) NOT NULL CHECK (interest_rate >= 0) Agreed rate as decimal
term_months int NULL for revolving facilities
repayment_frequency text NOT NULL CHECK (repayment_frequency IN ('WEEKLY','FORTNIGHTLY','MONTHLY'))
repayment_amount numeric(18,2) NOT NULL Scheduled repayment amount
next_repayment_date date
loan_status text NOT NULL CHECK (loan_status IN ('PENDING_DISBURSEMENT','ACTIVE','ARREARS','DEFAULT','WRITE_OFF_PENDING','PAID_OFF','WRITTEN_OFF','RESTRUCTURED')) PENDING_DISBURSEMENT = post-MOD-001-account-create pre-disbursement (AD k-6); WRITE_OFF_PENDING = write-off proposed awaiting CFO approval (AD k-7)
arrears_days int NOT NULL DEFAULT 0 CHECK (arrears_days >= 0) Days past due
arrears_amount numeric(18,2) NOT NULL DEFAULT 0.00
last_alerted_threshold int NOT NULL DEFAULT 0 AD k-8: highest arrears threshold for which a boundary-crossing alert has been sent; prevents duplicate alerts on the same boundary
disbursed_at timestamptz
maturity_date date
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL

Indexes: - idx_loan_accounts_party_id on (party_id) - idx_loan_accounts_loan_status on (loan_status) WHERE loan_status NOT IN ('PAID_OFF','WRITTEN_OFF') - idx_loan_accounts_arrears on (arrears_days) WHERE arrears_days > 0 - idx_loan_accounts_account_id on (account_id)


credit.collateral_register

Register of security interests and collateral held against credit facilities, maintained by MOD-066. Required for LGD calculation in ECL provisioning and enforcement proceedings.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
collateral_type text NOT NULL CHECK (collateral_type IN ('REAL_PROPERTY','VEHICLE','TERM_DEPOSIT','GUARANTEE','BUSINESS_ASSETS','OTHER'))
description text NOT NULL
estimated_value numeric(18,2) NOT NULL
currency char(3) NOT NULL
valuation_date date NOT NULL
valuation_source text NOT NULL Valuer or estimation method
security_interest_type text NOT NULL CHECK (security_interest_type IN ('MORTGAGE','PPSR','GUARANTEE','PLEDGE','LIEN'))
registration_reference text NULL Registration number; authority qualified via registration_authority
collateral_status text NOT NULL CHECK (collateral_status IN ('ACTIVE','RELEASED','ENFORCED','PENDING_RELEASE'))
registered_at timestamptz NULL When security interest was registered
released_at timestamptz NULL
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU')) Regulatory split
haircut_pct numeric(5,4) NOT NULL CHECK (haircut_pct >= 0 AND haircut_pct < 1) Strict upper bound — 1.0 would zero net value
net_collateral_value numeric(18,2) NOT NULL CHECK (net_collateral_value >= 0) Denormalised: estimated_value × (1 − haircut_pct)
product_policy_max_ltv numeric(8,6) NOT NULL CHECK (product_policy_max_ltv > 0 AND product_policy_max_ltv <= 1) Locked at registration from LTV policy
ltv_ratio numeric(8,6) NULL CHECK (ltv_ratio >= 0) Denormalised from latest valuation
ltv_breach boolean NOT NULL DEFAULT false true when ltv_ratio > product_policy_max_ltv
registration_expiry_date date NULL FR-198 alerting; drives renewal_tasks
registration_authority text NULL e.g. PPSR-NZ, PPSR-AU, LINZ
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL Touch trigger maintains

Indexes: - idx_collateral_register_loan_account_id on (loan_account_id) - idx_collateral_register_party_id on (party_id) - idx_collateral_register_status on (collateral_status) WHERE collateral_status = 'ACTIVE'

Column rename (MOD-066 V001): ppsr_registration_number was renamed to registration_reference — the PPSR name was too narrow; the authority is now captured in registration_authority.


credit.collateral_valuations

Append-only Cat 1 immutable revaluation log. One row per valuation event; latest row per collateral_id is the current valuation. The credit.collateral_register denormalised fields are kept in sync by the record-valuation handler. Managed by MOD-066.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
collateral_id uuid NOT NULL REFERENCES credit.collateral_register(id)
valuation_date date NOT NULL
estimated_value numeric(18,2) NOT NULL CHECK (estimated_value >= 0)
currency char(3) NOT NULL Snapshot self-contained (k-2 amendment)
valuation_source text NOT NULL Valuer or estimation method
haircut_pct_applied numeric(5,4) NOT NULL CHECK (haircut_pct_applied >= 0 AND haircut_pct_applied < 1)
net_collateral_value numeric(18,2) NOT NULL CHECK (net_collateral_value >= 0)
outstanding_principal_at_valuation numeric(18,2) NOT NULL CHECK (outstanding_principal_at_valuation >= 0) Captured at-valuation moment
ltv_ratio numeric(8,6) NULL CHECK (ltv_ratio >= 0)
ltv_breach boolean NOT NULL DEFAULT false
product_policy_max_ltv numeric(8,6) NOT NULL LTV policy at time of valuation
model_version text NOT NULL e.g. haircut-v1.0.0+ltv-policy-v1.0.0
valued_by_staff_id text NULL NULL for SYSTEM-recorded valuations
trace_id uuid NOT NULL
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - UNIQUE (collateral_id, valuation_date, valuation_source) — dedup guard

Immutability: Cat 1 — trg_collateral_valuations_immutable BEFORE UPDATE OR DELETE raises insufficient_privilege. Grants: SELECT, INSERT to app_user (no UPDATE/DELETE).


credit.collateral_release_requests

Mutable release governance table. Application-layer immutability for terminal states (APPROVED, REJECTED). 3-state machine: PENDING → APPROVED | REJECTED. Managed by MOD-066.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
collateral_id uuid NOT NULL REFERENCES credit.collateral_register(id)
status text NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','APPROVED','REJECTED'))
requested_by_staff_id text NOT NULL
requested_reason text NOT NULL
approved_by_staff_id text NULL Populated on terminal state
approval_committee_ref text NULL Governance ticket reference
decision_reason text NULL Populated on terminal state
requested_at timestamptz NOT NULL DEFAULT now()
decided_at timestamptz NULL Populated on terminal state
trace_id uuid NOT NULL

Biconditional CHECK chk_release_request_governance:

(status = 'PENDING' AND approved_by_staff_id IS NULL AND decided_at IS NULL)
OR
(status IN ('APPROVED','REJECTED') AND approved_by_staff_id IS NOT NULL AND decided_at IS NOT NULL)


credit.collateral_renewal_tasks

Mutable operations work queue for collateral registration expiry tracking. Daily-expiry-sweep Lambda creates tasks at 90/30/7 day windows. Managed by MOD-066.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
collateral_id uuid NOT NULL REFERENCES credit.collateral_register(id)
days_until_expiry int NOT NULL CHECK (days_until_expiry IN (90, 30, 7)) FR-198 alerting windows
expiry_date date NOT NULL Included in UNIQUE to allow multiple expiry cycles
status text NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','IN_PROGRESS','COMPLETED','CANCELLED'))
assigned_to_staff_id text NULL
trace_id uuid NOT NULL
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL Touch trigger maintains
completed_at timestamptz NULL

Unique constraint: (collateral_id, days_until_expiry, expiry_date) — prevents duplicate tasks per expiry cycle (k-4 amendment; expiry_date included so prior-cycle COMPLETED rows don't block the next cycle's tasks).


credit.property_security_details

Property-specific extension sidecar for credit.collateral_register rows where collateral_type = 'REAL_PROPERTY'. One-to-one with the MOD-066 row via collateral_id PK. Holds LINZ/LRS title reference, address, property subtype, and borrower intent — data that is meaningful only for real property and not appropriate in the generic collateral register. Managed by MOD-115.

Column Type Constraints Description
collateral_id uuid PK REFERENCES credit.collateral_register(id) One-to-one with the MOD-066 row
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id) Denormalised for fast FR-521 lookup
title_reference text NOT NULL LINZ / LRS title reference
property_address jsonb NOT NULL {street, suburb, city, postcode, country}
property_subtype text NOT NULL CHECK (property_subtype IN ('RESIDENTIAL','RURAL_RESIDENTIAL','APARTMENT','TOWNHOUSE'))
borrower_intent text NOT NULL CHECK (borrower_intent IN ('OWNER_OCCUPIER','INVESTOR')) RBNZ BS19 distinguishes max LVR by intent
registration_number_lr text NULL LR-specific registration number; separate from collateral_register.registration_reference which holds the PPSR ref
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL Touch trigger maintains

Notes: Mutable. Discharge updates credit.collateral_register.collateral_status = 'RELEASED' in MOD-066's table (same Neon DB, direct write by MOD-115's discharge handler — no staff approval flow, payoff-driven discharge is distinct from the MOD-066 committee release path).


credit.lvr_snapshots

Append-only Cat 1 immutable daily LVR snapshot per loan. Regulatory snapshots must not change after taking (RBNZ BS19 / APRA APS 220 audit requirement). One row per (loan_account_id, snapshot_date) — v1 assumes one primary property security per mortgage. Managed by MOD-115.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
collateral_id uuid NOT NULL REFERENCES credit.collateral_register(id)
snapshot_date date NOT NULL
outstanding_balance numeric(18,2) NOT NULL From credit.loan_accounts.outstanding_principal at snapshot time
current_valuation numeric(18,2) NOT NULL From latest credit.collateral_valuations at snapshot time
lvr_pct numeric(7,4) NOT NULL Unhaircut raw LVR = outstanding_balance / current_valuation; used for prudential reporting
lvr_band text NOT NULL CHECK (lvr_band IN ('<60','60-70','70-80','80-90','>90'))
policy_max_lvr numeric(7,4) NOT NULL Locked at snapshot time from LVR policy table (jurisdiction + borrower_intent)
policy_breach boolean NOT NULL DEFAULT false true when lvr_pct > policy_max_lvr
jurisdiction char(2) NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
borrower_intent text NOT NULL CHECK (borrower_intent IN ('OWNER_OCCUPIER','INVESTOR')) Denormalised so portfolio reports avoid JOIN to property_security_details
trigger_reason text NOT NULL CHECK (trigger_reason IN ('DAILY_SWEEP','REVALUATION','BALANCE_CHANGE','REGISTRATION')) What caused this snapshot to be written
trace_id uuid NOT NULL
created_at timestamptz NOT NULL DEFAULT now()

Unique constraint: (loan_account_id, snapshot_date) — DB-level idempotency for daily sweep replays. v1 assumes one primary property security per mortgage; a future migration would extend to (loan_account_id, collateral_id, snapshot_date) to support multiple securities per loan.

Immutability: Cat 1 — trg_lvr_snapshots_immutable BEFORE UPDATE OR DELETE raises insufficient_privilege.


credit.collections_cases

Collections workflow records for loans in arrears or default, managed by MOD-065. Tracks customer contact history, payment arrangements, and hardship assessments.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
party_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
case_reference text NOT NULL UNIQUE
arrears_days int NOT NULL Days past due at case opening
arrears_amount numeric(18,2) NOT NULL
case_status text NOT NULL CHECK (case_status IN ('OPEN','HARDSHIP_REVIEW','PAYMENT_ARRANGEMENT','LEGAL_REFERRAL','WRITTEN_OFF','RESOLVED','CLOSED'))
hardship_declared boolean NOT NULL DEFAULT false Customer has declared financial hardship
hardship_type text CHECK (hardship_type IN ('TEMPORARY','ONGOING','SERIOUS'))
payment_arrangement_amount numeric(18,2) Agreed reduced payment under arrangement
payment_arrangement_expiry date
assigned_to text Collections staff ID
opened_at timestamptz NOT NULL DEFAULT now()
resolved_at timestamptz
resolution_type text CHECK (resolution_type IN ('PAID_IN_FULL','ARRANGEMENT','WRITE_OFF','LEGAL','HARDSHIP_VARIATION'))
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL

Indexes: - idx_collections_cases_loan_account_id on (loan_account_id) - idx_collections_cases_status on (case_status) WHERE case_status NOT IN ('CLOSED','RESOLVED','WRITTEN_OFF') - idx_collections_cases_arrears_days on (arrears_days DESC) WHERE case_status = 'OPEN'


credit.repayment_schedules (MOD-065)

Amortisation schedule for each loan account. Mutable — MOD-065 restructure handler marks old PENDING/PARTIAL rows as RESCHEDULED and appends new rows at higher sequence numbers (AD k-3 rescheduling pattern).

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
sequence_number int NOT NULL CHECK (sequence_number > 0) Monotone within a loan; rescheduling appends at higher numbers
scheduled_date date NOT NULL
scheduled_principal numeric(18,2) NOT NULL CHECK (scheduled_principal >= 0)
scheduled_interest numeric(18,2) NOT NULL CHECK (scheduled_interest >= 0)
scheduled_total numeric(18,2) NOT NULL CHECK (scheduled_total >= 0)
paid_amount numeric(18,2) NOT NULL DEFAULT 0
paid_at timestamptz NULL
status text NOT NULL CHECK (status IN ('PENDING','PAID','PARTIAL','MISSED','RESCHEDULED'))
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL Touch trigger

Constraints: UNIQUE (loan_account_id, sequence_number). Indexes: (loan_account_id, scheduled_date); partial (status, scheduled_date) WHERE status IN ('PENDING','PARTIAL'). Trigger: trg_repayment_schedules_touch_updated_at (touch only — mutable table).


credit.collections_actions (MOD-065)

Append-only Cat 1 immutable audit log of all collections actions taken on a case. Every contact attempt, hardship declaration, arrangement agreement, and escalation creates a new row. ADR-048 Cat 1 immutable.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
case_id uuid NOT NULL REFERENCES credit.collections_cases(id)
action_type text NOT NULL CHECK (action_type IN ('REMINDER_SENT','CONTACT_ATTEMPTED','HARDSHIP_DECLARED','HARDSHIP_OUTCOME','ARRANGEMENT_AGREED','ESCALATION','WRITE_OFF_PROPOSED','RESTRUCTURE_APPLIED'))
channel text NOT NULL CHECK (channel IN ('EMAIL','SMS','APP','PHONE','LETTER','SYSTEM'))
staff_id text NULL NULL on system-generated actions (channel='SYSTEM'; AD k-4)
outcome text NULL
notes text NULL
policy_refs jsonb NOT NULL DEFAULT '["CRE-002","CRE-006","CON-003"]'
action_at timestamptz NOT NULL DEFAULT now()
trace_id uuid NULL ADR-031 propagation
created_at timestamptz NOT NULL DEFAULT now()

Indexes: (case_id, action_at DESC); (action_type, action_at DESC). Immutability: trg_collections_actions_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row(). ADR-048 Cat 1. 7-year retention via Snowflake-side CDC (AD k-9).


credit.bureau_enquiries

Inbound credit bureau enquiry log (MOD-128). One row per bureau call — including duplicate calls suppressed by FR-578's cache and primary-bureau failures with fallback used (FR-579 audit trail). Distinct from credit.credit_bureau_requests (MOD-059 outbound CCR submissions).

Column Type Constraints Description
enquiry_id uuid PK DEFAULT gen_random_uuid()
customer_id uuid NOT NULL Cross-domain ref to SD02 party.parties(party_id)
application_id uuid nullable NULL for account_review and collection enquiries
jurisdiction text NOT NULL CHECK (NZ, AU)
bureau text NOT NULL CHECK (equifax_au, experian_au, illion, centrix, equifax_nz)
enquiry_purpose text NOT NULL CHECK (credit_assessment, account_review, collection)
consent_reference uuid NOT NULL FK-by-convention to credit.bureau_consents(consent_id) until MOD-049 ships
request_at timestamptz NOT NULL DEFAULT now()
response_at timestamptz nullable
response_status text nullable CHECK (success, no_file, bureau_error, timeout)
credit_score int nullable Bureau-supplied; NULL on no_file/error/suppressed
bureau_reference text nullable Bureau's opaque reference for audit
report_payload jsonb NOT NULL DEFAULT '{}' Redacted inline; full payload accessible via s3_key
s3_key text nullable Encrypted-at-rest S3 object key for the full credit file
adverse_flags text[] NOT NULL DEFAULT '{}' Normalised via MOD-128's adverse-flag mapping
suppressed boolean NOT NULL DEFAULT false FR-578: true if returned from 90-day cache
cached_from_enquiry_id uuid nullable When suppressed=true, points to the source enquiry
primary_bureau_failed boolean NOT NULL DEFAULT false FR-579 audit
fallback_bureau_used text nullable Bureau used on fallback (FR-579 audit)
policy_ref text NOT NULL DEFAULT 'REP-010'
trace_id uuid nullable ADR-031 propagation
created_at timestamptz NOT NULL DEFAULT now()

Indexes: customer_id; (application_id) WHERE NOT NULL; request_at DESC; partial index on (customer_id, enquiry_purpose, bureau, request_at DESC) for FR-578 duplicate-suppression lookup.

Immutability (ADR-048 Cat 1): trg_credit_bureau_enquiries_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row().


credit.bureau_consents (temporary stand-in)

Stand-in for MOD-049 (SD08 open-banking consent management, Not started). Replaced by an AP-010 pattern 1 cross-domain read of MOD-049's published consent view once MOD-049 ships. Mutable (supports revocations).

Column Type Constraints Description
consent_id uuid PK DEFAULT gen_random_uuid()
customer_id uuid NOT NULL
purpose text NOT NULL CHECK ('credit_bureau_enquiry')
granted_at timestamptz NOT NULL
expires_at timestamptz nullable
revoked_at timestamptz nullable
source_channel text NOT NULL CHECK (CREDIT_APPLICATION, PRE_APPROVAL, BACK_OFFICE)
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL

Lifecycle: Dropped via a new MOD-128 V003 migration after MOD-049 ships and existing rows are migrated into MOD-049's consent store. See bank-credit/docs/handoffs/MOD-128-mod049-consent-migration.handoff.md.


credit.disclosure_acknowledgements (temporary stand-in)

Stand-in for MOD-050 (SD08 disclosure enforcement, Not started). Records that a customer was shown the full pre-approval offer terms (rate, term, total interest, total cost) and acknowledged them before accepting. ADR-048 Cat 1 — immutable once written. Replaced by a cross-domain read of MOD-050's disclosure store once MOD-050 ships.

Created by MOD-029 V001. See bank-credit/docs/handoffs/MOD-029-mod050-disclosure-migration.handoff.md for the retirement plan.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
application_id uuid NOT NULL REFERENCES credit.credit_applications(id)
party_id uuid NOT NULL Cross-domain ref to SD02 party.parties(party_id)
disclosure_type text NOT NULL CHECK (disclosure_type IN ('PRE_APPROVAL_OFFER')) Extended when MOD-050 takes over
content_version text NOT NULL Version of the offer disclosure template shown
content_hash text NOT NULL SHA-256 of the exact offer terms presented — rate, term, total interest, total cost
acknowledged_at timestamptz NOT NULL Timestamp of customer acknowledgement
channel text NOT NULL CHECK (channel IN ('APP','BRANCH','API','WEB'))
customer_session_id text Cognito session ID if APP channel
created_at timestamptz NOT NULL DEFAULT now()

Immutability: ADR-048 Cat 1 — trg_disclosure_acknowledgements_immutable rejects all UPDATE and DELETE. The row is the CON-004 GATE evidence for the acceptance path.

Indexes: - idx_disclosure_acks_application_id on (application_id) - idx_disclosure_acks_party_id on (party_id, acknowledged_at DESC)


credit.bureau_config

Operator-managed bureau routing per (jurisdiction, product_type, enquiry_purpose). UNIQUE constraint on those three columns. Seed-loaded for AU credit_assessment (Equifax AU primary, illion fallback) and NZ credit_assessment (Centrix primary). Mutable.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
jurisdiction text NOT NULL CHECK (NZ, AU)
product_type text NOT NULL e.g. PERSONAL_LOAN, HOME_LOAN
enquiry_purpose text NOT NULL
primary_bureau text NOT NULL
fallback_bureau text nullable NULL = no fallback
updated_at timestamptz NOT NULL

credit.idempotency_keys

Shared SD05 idempotency store. Prevents duplicate Lambda invocations from producing duplicate side-effects across all SD05 modules. PK is (key, module_id). 24-hour TTL enforced by opportunistic DELETE at write time. First created by MOD-128; reused by MOD-027, MOD-028, MOD-029, MOD-031, MOD-059, MOD-065, and others.

Column Type Constraints Description
key text NOT NULL Caller-supplied idempotency key (e.g. request UUID)
module_id text NOT NULL e.g. MOD-128
response_payload jsonb NOT NULL Cached response body
created_at timestamptz NOT NULL DEFAULT now() TTL anchor

PK: (key, module_id) — allows the same key to be used by different modules without collision.


credit.mortgage_rate_periods (MOD-116)

Fixed and variable rate period history per mortgage loan. One active row per loan at any time (PARTIAL UNIQUE). State machine: VARIABLE → FIXED → EXPIRING → EXPIRED. Mutable — status transitions are updated in place; terminal rows (EXPIRED, VARIABLE) are immutable by application logic. Managed by MOD-116.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
rate_type text NOT NULL CHECK (rate_type IN ('VARIABLE','FIXED'))
status text NOT NULL CHECK (status IN ('VARIABLE','FIXED','EXPIRING','EXPIRED')) Current state machine position
interest_rate numeric(8,6) NOT NULL CHECK (interest_rate >= 0) Rate applicable for this period
fixed_term_months int NULL NULL for VARIABLE periods
start_date date NOT NULL Period start
end_date date NULL NULL until fixed term is set; set at election
elected_by text NULL CHECK (elected_by IN ('CUSTOMER','AUTO_REVERSION')) NULL for initial VARIABLE row
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL Touch trigger

Unique constraint: PARTIAL UNIQUE (loan_account_id) WHERE status IN ('VARIABLE','FIXED','EXPIRING') — only one active rate period per loan at a time. Indexes: (loan_account_id, start_date DESC); partial (status, end_date) WHERE status = 'EXPIRING' for daily-fixed-rate-sweep boundary detection.


credit.mortgage_notifications (MOD-116)

Notification log for all mortgage lifecycle notifications. One row per dispatched notification; idempotent per (loan_account_id, notification_type, rate_period_id). In v1 audit-only mode (MOD-063 not yet built): row is written with dispatched_at = now() but no customer message is sent — SNS alarm publishes to MOD-076 observability intake only. Mutable (status can transition from PENDING to DISPATCHED). Managed by MOD-116.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
notification_type text NOT NULL CHECK (notification_type IN ('FIXED_RATE_EXPIRY_90','FIXED_RATE_EXPIRY_60','FIXED_RATE_EXPIRY_30','RATE_REVERTED','BREAK_COST_QUOTE','DISCHARGE_CONFIRMATION','ARREARS_DAY1','ARREARS_DAY7','ARREARS_DAY30'))
rate_period_id uuid NULL REFERENCES credit.mortgage_rate_periods(id) NULL for notifications not tied to a specific rate period
channel text NOT NULL DEFAULT 'STUB' CHECK (channel IN ('STUB','EMAIL','SMS','PUSH','LETTER')) STUB = v1 audit-only mode until MOD-063 ships
status text NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','DISPATCHED','FAILED'))
dispatched_at timestamptz NULL Set when status transitions to DISPATCHED
trace_id uuid NULL ADR-031 propagation
created_at timestamptz NOT NULL DEFAULT now()

Unique constraint: UNIQUE (loan_account_id, notification_type, rate_period_id) — at most one of each notification type per rate period; prevents duplicate 90/60/30-day alerts. Indexes: (loan_account_id, created_at DESC); partial (status, created_at) WHERE status = 'PENDING' for retry sweep.


credit.break_cost_disclosures (MOD-116)

Per-disclosure record for fixed rate break cost quotes. Append-only by application logic — new quote = new row. CON-005 GATE evidence: accepted_at must be non-null before any early repayment posting is submitted to MOD-001. Mutable (only accepted_at is written post-creation). Managed by MOD-116.

Column Type Constraints Description
id uuid PK DEFAULT gen_random_uuid()
idempotency_key text NOT NULL Caller-supplied; typically customer discharge reference
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
rate_period_id uuid NOT NULL REFERENCES credit.mortgage_rate_periods(id) The active FIXED period at calculation time
outstanding_balance numeric(18,2) NOT NULL Loan balance at calculation time
remaining_term_months int NOT NULL Months remaining in fixed period at calculation time
reinvestment_rate numeric(8,6) NOT NULL Swap rate used (tenor-matched; see CRE-006 §9)
break_cost_amount numeric(18,2) NOT NULL CHECK (break_cost_amount >= 0) Calculated break cost; 0 if reinvestment_rate >= contract_rate
contract_rate numeric(8,6) NOT NULL The loan's fixed interest_rate at calculation time
currency text NOT NULL CHECK (currency IN ('NZD','AUD'))
expires_at timestamptz NOT NULL Quote validity window; v1 default: created_at + interval '5 days', AppConfig-overridable
disclosed_at timestamptz NOT NULL DEFAULT now() Timestamp of quote publication to customer
accepted_at timestamptz NULL Set when customer formally accepts the break cost — CON-005 GATE
accepted_by_customer_id uuid NULL Customer party_id on acceptance; cross-domain ref, no FK
created_at timestamptz NOT NULL DEFAULT now()

Unique constraint: UNIQUE (idempotency_key) — quote requests are idempotent. CON-005 GATE: Application logic in discharge-mortgage Lambda checks accepted_at IS NOT NULL AND now() < expires_at before proceeding. Expired quotes must be recalculated. Indexes: (loan_account_id, created_at DESC); (idempotency_key).


credit.overdraft_facilities (MOD-117)

Master record for each linked overdraft facility. One active facility per transaction account (UNIQUE on account_id). Paired with a credit.loan_accounts row (product_type = 'OVERDRAFT') so that MOD-031 ECL, MOD-059 CCR, and MOD-030 IFRS 9 staging see overdraft balances without special casing. current_drawn_balance mirrors the drawn balance (v1 stub pending MOD-003 integration); any write to this column MUST also update loan_accounts.outstanding_principal in the same transaction.

Column Type Constraints Description
facility_id uuid PK DEFAULT gen_random_uuid()
account_id uuid NOT NULL cross-domain ref to bank_core core.accounts(id) — app-layer enforced, no FK
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id) Paired OVERDRAFT product_type row; kept in sync for ECL/CCR/IFRS 9 pipeline
approved_limit numeric(18,2) NOT NULL Board/credit-approved limit ceiling
current_limit numeric(18,2) NOT NULL Active limit; may differ from approved_limit during reduction notice period
current_drawn_balance numeric(18,2) NOT NULL DEFAULT 0 v1 stub mirror — updated by update-drawn-balance Lambda and atomically with loan_accounts.outstanding_principal
consecutive_drawn_days int NOT NULL DEFAULT 0 FR-532 counter: calendar days the balance has been continuously negative; reset after 5 consecutive positive-balance days (AppConfig: OVERDRAFT_HARDSHIP_RESET_GRACE_DAYS default 5)
interest_rate_pct numeric(8,5) NOT NULL Annual interest rate as percentage (see CRE-006 §10 for v1 defaults)
facility_fee numeric(18,2) NOT NULL Monthly facility fee if any day in month had negative balance (see CRE-006 §10)
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','suspended','closed'))
review_date date NOT NULL Next scheduled affordability review
last_assessment_id uuid NULL FK-by-convention to credit.affordability_assessments; cross-domain ref, no FK
activated_at timestamptz NOT NULL
closed_at timestamptz NULL
created_at timestamptz NOT NULL DEFAULT now()

Constraints: UNIQUE (account_id) — one active facility per transaction account. Indexes: (account_id) WHERE status = 'active'; (loan_account_id).


credit.overdraft_daily_accruals (MOD-117)

One row per facility per calendar day on which the balance was negative. Populated by the daily-accrual-sweep Lambda. posted flag flips to true when the accrual is included in a monthly interest charge (mutable on that single column only). UNIQUE on (facility_id, accrual_date) prevents duplicate sweep runs.

Column Type Constraints Description
accrual_id uuid PK DEFAULT gen_random_uuid()
facility_id uuid NOT NULL REFERENCES credit.overdraft_facilities(facility_id)
accrual_date date NOT NULL Calendar date of accrual
drawn_balance numeric(18,2) NOT NULL Absolute drawn balance at end of day
daily_interest numeric(18,6) NOT NULL drawn_balance × (interest_rate_pct / 100 / 365)
posted boolean NOT NULL DEFAULT false Set to true when included in monthly-close charge
created_at timestamptz NOT NULL DEFAULT now()

Constraints: UNIQUE (facility_id, accrual_date).


credit.overdraft_events (MOD-117)

Cat 1 append-only audit log of all significant lifecycle events on the facility. Immutability trigger prevents any UPDATE or DELETE (same pattern as credit.collateral_valuations). Retained for 7 years via Snowflake CDC.

Column Type Constraints Description
event_id uuid PK DEFAULT gen_random_uuid()
facility_id uuid NOT NULL REFERENCES credit.overdraft_facilities(facility_id)
event_type text NOT NULL CHECK (event_type IN ('facility_created','limit_increased','limit_reduced','limit_suspended','interest_charged','fee_assessed','fee_waived','hardship_flagged','unarranged_detected','facility_closed'))
event_data jsonb NULL Structured payload per event_type (e.g. old/new limit for limit changes, consecutive_drawn_days for hardship_flagged)
created_at timestamptz NOT NULL DEFAULT now()

Indexes: (facility_id, created_at DESC).


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-credit.

Immutability triggers

Table Trigger Category
credit.credit_decisions trg_credit_decisions_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
credit.credit_bureau_requests trg_credit_bureau_requests_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
credit.affordability_assessments trg_affordability_assessments_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
credit.ecl_provisions trg_ecl_provisions_immutable — BEFORE UPDATE OR DELETE, calls fn_immutable_row() Cat 1
credit.bureau_enquiries trg_credit_bureau_enquiries_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() Cat 1
credit.credit_scores trg_credit_scores_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() Cat 1
credit.risk_scores_mirror trg_risk_scores_mirror_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() Cat 1
credit.disclosure_acknowledgements trg_disclosure_acknowledgements_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-029 V001). CON-004 GATE evidence for pre-approval acceptance; 7-year retention. Cat 1
credit.ifrs9_stage_allocations trg_ifrs9_stage_allocations_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-030 V001). IFRS 9 stage history; CRE-006 compliance. Cat 1
credit.collections_actions trg_collections_actions_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-065 V001). Collections audit log; CRE-002 / CON-003 compliance; 7-year retention via Snowflake CDC. Cat 1
credit.collateral_valuations trg_collateral_valuations_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-066 V001). Per-revaluation LTV snapshot; CRE-001 / CRE-005 compliance. Cat 1
credit.lvr_snapshots trg_lvr_snapshots_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-115 V001). Daily LVR snapshot; RBNZ BS19 / APRA APS 220 audit; REP-002 compliance. Cat 1
credit.overdraft_events trg_overdraft_events_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-117 V001). Overdraft lifecycle audit log; CON-008 / CRE-002 compliance; 7-year retention via Snowflake CDC. Cat 1
credit.break_cost_calculations trg_break_cost_calculations_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_break_cost_immutable() (MOD-163 V001). DELETE always blocked; UPDATE whitelisted on status, disclosure_acknowledgement_id, disclosure_content_hash, acknowledged_at only. CRE-009 CALC + CON-005 GATE evidence; break-cost quote history. Cat 1
credit.loan_variation_events trg_loan_variation_events_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row() (MOD-132 V001). REP-004 LOG evidence; responsible lending audit; 7-year retention. Cat 1

Other tables (credit.credit_applications, credit.loan_accounts, credit.collateral_register, credit.collateral_release_requests, credit.collateral_renewal_tasks, credit.property_security_details, credit.collections_cases, credit.repayment_schedules, credit.ecl_runs, credit.bureau_consents, credit.bureau_config, credit.idempotency_keys, credit.bureau_submission_batches, credit.bureau_disputes, credit.mortgage_rate_periods, credit.mortgage_notifications, credit.break_cost_disclosures, credit.overdraft_facilities, credit.overdraft_daily_accruals, credit.loan_variations) are mutable by design. credit.ecl_runs has application-layer immutability for terminal states (COMPLETE, FAILED) enforced by the Lambda handler, not a DB trigger. — application status, arrears state, loan lifecycle, bureau submission/dispute status transitions, and repayment schedule restructures are updated by the owning modules.

CHECK constraints

Table Column Constraint
credit.loan_accounts interest_rate CHECK (interest_rate >= 0)
credit.loan_accounts arrears_days CHECK (arrears_days >= 0)
credit.ecl_provisions pd_12m CHECK (pd_12m >= 0 AND pd_12m <= 1)
credit.ecl_provisions pd_lifetime CHECK (pd_lifetime >= 0 AND pd_lifetime <= 1)
credit.ecl_provisions lgd CHECK (lgd >= 0 AND lgd <= 1)
credit.break_cost_disclosures break_cost_amount CHECK (break_cost_amount >= 0)
credit.break_cost_disclosures reinvestment_rate CHECK (reinvestment_rate >= 0)
credit.mortgage_rate_periods interest_rate CHECK (interest_rate >= 0)
credit.overdraft_facilities current_drawn_balance CHECK (current_drawn_balance >= 0)
credit.overdraft_facilities consecutive_drawn_days CHECK (consecutive_drawn_days >= 0)
credit.overdraft_facilities interest_rate_pct CHECK (interest_rate_pct >= 0)
credit.overdraft_facilities facility_fee CHECK (facility_fee >= 0)
credit.overdraft_daily_accruals drawn_balance CHECK (drawn_balance >= 0)
credit.overdraft_daily_accruals daily_interest CHECK (daily_interest >= 0)
credit.break_cost_calculations chk_binding_has_component_id CHECK (calculation_type = 'INDICATIVE' OR component_id IS NOT NULL)
credit.break_cost_calculations chk_binding_has_valid_until CHECK (calculation_type = 'INDICATIVE' OR valid_until IS NOT NULL)
credit.break_cost_calculations chk_acknowledged_has_disclosure_id CHECK (status NOT IN ('ACKNOWLEDGED','APPLIED') OR disclosure_acknowledgement_id IS NOT NULL)
credit.break_cost_calculations chk_acknowledged_has_timestamp CHECK (status NOT IN ('ACKNOWLEDGED','APPLIED') OR acknowledged_at IS NOT NULL)

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

Rule Reason Owner
Credit score threshold gates Score floor and risk-grade bands are AppConfig-configurable and depend on external bureau model outputs MOD-029 Lambda
KYC gate for credit Requires CDD tier from SD02 (different Neon database) MOD-029 Lambda
HEM benchmark lookup HEM data is sourced from bank-risk-platform Snowflake at assessment time MOD-027 Lambda

Construction loan tables (MOD-121)

Added by MOD-121 (construction loan drawdown engine). All three tables live in the credit schema on the same Neon cluster as the rest of SD05.

credit.construction_schedules (mutable)

One schedule per mortgage. The total_drawn column is the authoritative source of truth for drawn principal; it is atomically mirrored to loan_accounts.outstanding_principal on every drawdown (AD k-6).

Column Type Notes
id uuid PK
loan_account_id uuid NOT NULL UNIQUE FK loan_accounts(id) One schedule per mortgage
total_facility numeric(18,2) > 0
total_drawn numeric(18,2) 0 ≤ v ≤ total_facility; mirrored to loan_accounts.outstanding_principal
construction_end_date date NOT NULL
conversion_date date NULL Set on completion
status text CHECK (active, complete, defaulted)
jurisdiction char(2) CHECK (NZ, AU)
currency char(3)
source_application_id uuid NOT NULL
trace_id uuid NOT NULL
created_at timestamptz
updated_at timestamptz

credit.construction_tranches (mutable)

Up to 10 tranches per schedule (tranche_number is UNIQUE with schedule_id). The sum of all tranche_amount values must equal schedule.total_facility.

Column Type Notes
id uuid PK
schedule_id uuid NOT NULL FK construction_schedules(id)
tranche_number int 1..10; UNIQUE with schedule_id
tranche_amount numeric(18,2) > 0; sum = schedule.total_facility
milestone_description text NOT NULL
status text CHECK (pending, inspection_requested, certified, drawn, lapsed)
certification_date date NULL
certifier_reference text NULL
drawdown_date date NULL
posting_id uuid NULL MOD-001 posting_id when drawn
trace_id uuid
created_at timestamptz
updated_at timestamptz

Cross-column constraints:

Constraint Rule
chk_construction_tranches_drawn_state drawdown_date + posting_id only when status = 'drawn' (DB-layer enforcement of FR-545)
chk_construction_tranches_certified_state Certification fields (certification_date, certifier_reference) only when status ∈ {certified, drawn, lapsed}

credit.construction_events (Cat 1 immutable)

Append-only audit log via the shared credit.fn_immutable_row() trigger. 7-year retention via Snowflake CDC (MOD-042).

event_type enum value Trigger
schedule_created create-schedule handler
milestone_inspection_requested certify-tranche handler (inspection_requested state)
milestone_certified certify-tranche handler (certified state)
milestone_lapsed daily-expiry-sweep handler
drawdown_posted request-drawdown handler
lvr_refreshed loan_balance_updated consumer
amortisation_triggered construction_phase_completed consumer
schedule_completed request-drawdown / daily-expiry-sweep (all tranches drawn or end date reached)
schedule_defaulted daily-expiry-sweep (status transition)

Flexible Loan Facility tables (MOD-162)

Added by MOD-162 (Loan facility & component manager). Both tables live in the credit schema. They implement the layered model: facility envelopes wrap components, each component is backed by a loan_accounts row (k-1 ruling).

credit.loan_facilities (mutable)

Facility envelope per Flexible Loan Facility (PRD-024). One record per approved flexible credit decision.

Column Type Notes
id uuid PK
customer_id uuid NOT NULL cross-domain ref to SD02 party.parties(party_id)
credit_decision_id uuid NOT NULL UNIQUE FK credit_decisions(id) One facility per decision
application_id uuid NOT NULL FK credit_applications(id)
facility_limit numeric(18,2) NOT NULL CHECK (> 0)
currency char(3) NOT NULL
expiry_date date NOT NULL
jurisdiction char(2) NOT NULL CHECK (NZ, AU)
effective_interest_rate numeric(8,6) NOT NULL Σ(principal × rate) / Σ(principal) — recomputed on every component event (k-6)
status text NOT NULL CHECK (ACTIVE, EXPIRED, CANCELLED)
master_agreement_ref text NULL
trace_id uuid NOT NULL
created_at timestamptz NOT NULL
last_updated timestamptz NOT NULL

credit.loan_facility_components (append-only; terminal-state Cat 1 immutable)

One row per component state transition (k-5 append-only state machine). The latest row at (facility_id, component_seq, created_at DESC) is the current state of that component. Terminal-state rows (MATURED, PREPAID, CANCELLED) are immutable via credit.fn_component_terminal_immutable() trigger (BEFORE UPDATE OR DELETE).

Column Type Notes
id uuid PK
facility_id uuid NOT NULL FK loan_facilities(id)
component_seq int NOT NULL Sequence number within facility
loan_account_id uuid NOT NULL FK loan_accounts(id) k-1 layered model: one loan_account per component
component_type text NOT NULL CHECK (FIXED, FLOATING)
status text NOT NULL CHECK (PENDING, ACTIVE, MATURED, PREPAID, CANCELLED)
rate_benchmark text NULL CHECK (BKBM, BBSY) FLOATING only; NULL for FIXED
benchmark_margin numeric(8,6) NULL FLOATING only; NULL for FIXED
trigger_reason text NOT NULL 7 values including RATE_REPRICING, MATURITY, FULL_PREPAYMENT, CANCELLED
previous_component_id uuid NULL FK self Prior state row
model_version text NOT NULL Schema version for event replay
trace_id uuid NOT NULL
created_at timestamptz NOT NULL

Cross-column constraint:

Constraint Rule
chk_component_floating_metadata FLOATING requires rate_benchmark and benchmark_margin; FIXED forbids both

credit.break_cost_calculations (Cat 1 immutable — frozen columns; whitelisted status transitions)

Break-cost quote history for MOD-163. One row per indicative or binding calculation. Binding rows gate fixed-rate component prepayment via CON-005.

Column Type Notes
id uuid PK gen_random_uuid()
calculation_type text NOT NULL 'INDICATIVE' | 'BINDING'
loan_facility_id uuid NOT NULL FK loan_facilities(id)
component_id uuid NULL FK loan_facility_components(id) Required for BINDING (k-8); NULL = whole-facility indicative
party_id text NOT NULL
contracted_rate numeric(7,4) NOT NULL Annual decimal e.g. 0.0550
current_market_rate numeric(7,4) NOT NULL Annual decimal
market_rate_as_of timestamptz NOT NULL When MOD-085 last priced
market_rate_source text NOT NULL e.g. "MOD-085:tenor_24m:rbnz_overnight"
outstanding_principal numeric(15,2) NOT NULL
remaining_term_months int NOT NULL Non-negative
discount_rate numeric(7,4) NOT NULL Annual decimal; v1 = current_market_rate
annuity_factor numeric(20,10) NOT NULL Audit field
break_cost numeric(15,2) NOT NULL Signed: positive = customer pays; negative = break benefit (k-7)
formula_version text NOT NULL DEFAULT 'v1.0.0' k-1
valid_until timestamptz NULL NULL for INDICATIVE; BINDING = now + 5 NZ business days (k-9)
disclosure_acknowledgement_id uuid NULL MOD-050 disclosure id
disclosure_content_hash text NULL SHA-256 of canonical JSON incl. formula_version (k-4 amendment)
acknowledged_at timestamptz NULL
status text NOT NULL DEFAULT 'ACTIVE' ACTIVE | EXPIRED | SUPERSEDED | ACKNOWLEDGED | APPLIED | CANCELLED (k-11)
idempotency_key text NOT NULL
trace_id text NOT NULL
created_at timestamptz NOT NULL DEFAULT now()

Immutability: trg_break_cost_calculations_immutable (BEFORE UPDATE OR DELETE, MOD-163 V001) — DELETE always blocked; UPDATE permitted only on status, disclosure_acknowledgement_id, disclosure_content_hash, acknowledged_at. Status machine: ACTIVE → EXPIRED (nightly cron) | SUPERSEDED (k-12: atomic override when new binding for same component) | ACKNOWLEDGED (CON-005 GATE) | APPLIED (MOD-065 at prepayment) | CANCELLED. ADR-048 Cat 1.


Loan variation tables (MOD-132)

Added by MOD-132 (loan restructure & variation workflow). Both tables live in the credit schema on the same Neon cluster. MOD-132 V001 creates both tables.

credit.loan_variations (mutable)

Mutable workflow state machine record for each loan variation request. One row per variation request; status transitions are updated in place via UPDATE. The full audit trail lives in credit.loan_variation_events (Cat 1 immutable). Reuses credit.idempotency_keys (module_id='MOD-132', 24h TTL) for duplicate-submission protection.

Column Type Constraints Description
variation_id uuid PRIMARY KEY DEFAULT gen_random_uuid()
loan_account_id uuid NOT NULL REFERENCES credit.loan_accounts(id)
variation_type text NOT NULL CHECK (variation_type IN ('term_extension','frequency_change','rate_type_switch','repayment_restructure','capitalisation_of_arrears','early_repayment'))
previous_terms jsonb NOT NULL Full snapshot of loan terms before variation (rate, term, frequency, repayment amount, rate type, term end date)
proposed_terms jsonb NOT NULL Requested new terms
status text NOT NULL DEFAULT 'requested' CHECK (status IN ('requested','assessing','assessed','disclosed','confirmed','rejected','expired')) Workflow state machine
assessment_required bool NOT NULL DEFAULT false FR-589: true for material variations (term extension >12m, capitalisation of arrears, repayment restructure)
break_cost_required bool NOT NULL DEFAULT false True when variation involves fixed-rate period exit
credit_check_id uuid MOD-029 credit_decisions ref; populated when assessment_required = true
disclosure_id uuid MOD-050 disclosure ref; populated when disclosure dispatched
break_cost_calculation_id uuid MOD-163 break_cost_calculations ref; populated when break_cost_required = true
break_cost_acknowledgement_id uuid MOD-050 acknowledgement ref; required on CONFIRMED path when break_cost_required = true (CON-005 GATE)
acknowledged_at timestamptz Set when break cost acknowledged
requested_by_party_id text NOT NULL party_id of customer or agent submitting the variation
requested_by_type text NOT NULL CHECK (requested_by_type IN ('CUSTOMER','AGENT'))
agent_id text Populated when requested_by_type = 'AGENT'; NULL for customer-initiated variations
case_id text MOD-053 case ref; populated on agent escalation
materiality_rules_version text NOT NULL Version of the materiality table applied at request time (e.g. 'v1.0.0') — audit immutability of the decision
rejection_reason text Human-readable rejection detail
rejection_source text CHECK (rejection_source IN ('CREDIT_DECLINED','CUSTOMER_DECLINED','AGENT_REJECTED','EXPIRED'))
idempotency_key text NOT NULL Client-supplied key; deduplicated via credit.idempotency_keys
trace_id text NOT NULL ADR-031 trace propagation
created_at timestamptz NOT NULL DEFAULT now()
updated_at timestamptz NOT NULL DEFAULT now() Touch-updated on every status transition
expires_at timestamptz TTL for disclosed state (5 business days); swept daily by expire-disclosed-sweep Lambda

Indexes: - PRIMARY KEY (variation_id) - idx_loan_variations_loan_account_id on (loan_account_id) - idx_loan_variations_status_active partial WHERE status NOT IN ('confirmed','rejected','expired') - Partial UNIQUE uniq_loan_variations_one_inflight on (loan_account_id, status) WHERE status IN ('requested','assessing','assessed','disclosed') — enforces one in-flight variation per loan account; second concurrent submission returns 409 IN_FLIGHT_VARIATION_EXISTS (k-8)

State machine:

requested
  ├─ assessment_required + break_cost_required:  → assessing → assessed → disclosed → confirmed
  ├─ assessment_required only:                   → assessing → assessed → disclosed → confirmed
  ├─ break_cost_required only:                   → disclosed → confirmed
  └─ neither:                                    → disclosed → confirmed
any non-terminal → rejected (terminal)
disclosed → expired (terminal, TTL 5 business days, set by expire-disclosed-sweep)


credit.loan_variation_events (Cat 1 immutable)

Append-only audit log of all loan variation lifecycle events. FR-592 REP-004 LOG evidence — available for regulatory examination without reconstruction. Every status transition and gate event creates a row. ADR-048 Cat 1 — UPDATE and DELETE blocked by trigger.

Column Type Constraints Description
event_id uuid PRIMARY KEY DEFAULT gen_random_uuid()
variation_id uuid NOT NULL REFERENCES credit.loan_variations(variation_id)
event_type text NOT NULL CHECK (event_type IN ('REQUESTED','ASSESSMENT_INVOKED','ASSESSMENT_APPROVED','ASSESSMENT_DECLINED','DISCLOSURE_DISPATCHED','BREAK_COST_CALCULATED','BREAK_COST_ACKNOWLEDGED','CONFIRMED','REJECTED','EXPIRED','CASE_ESCALATED'))
actor_party_id text NOT NULL party_id of the actor (customer, agent, or system module ID)
actor_type text NOT NULL CHECK (actor_type IN ('CUSTOMER','AGENT','SYSTEM'))
detail jsonb NOT NULL Event-specific payload: before/after term deltas, reason codes, referenced IDs
trace_id text NOT NULL ADR-031 trace propagation
created_at timestamptz NOT NULL DEFAULT now()

Indexes: - idx_loan_variation_events_variation_id on (variation_id, created_at DESC) - idx_loan_variation_events_loan_account on (variation_id) — via JOIN to loan_variations for account-level audit sweep

Immutability: trg_loan_variation_events_immutable — BEFORE UPDATE OR DELETE, calls credit.fn_immutable_row(). ADR-048 Cat 1. 7-year retention via Snowflake CDC (REP-004 responsible lending audit requirement).


Cross-domain references

This table Column References Notes
credit.credit_applications party_id SD02 party.parties(party_id) Applications require a verified party
credit.credit_decisions party_id SD02 party.parties(party_id)
credit.loan_accounts account_id SD01 accounts.accounts(id) Loan funds flow through the core banking account
credit.ecl_provisions gl_posting_id SD01 accounts.postings(id) Cross-domain reference — no FK enforced (different Neon databases). ECL movements posted to GL via MOD-001.
credit.overdraft_facilities account_id SD01 accounts.accounts(id) Cross-domain reference — no FK enforced (different Neon databases). App-layer enforced.
credit.loan_facility_components loan_account_id loan_accounts(id) k-1 layered model FK — each component is backed by a loan_account row
credit.break_cost_calculations loan_facility_id loan_facilities(id) FK within SD05 credit schema
credit.break_cost_calculations component_id loan_facility_components(id) Nullable; required for BINDING quotes (k-8)

Consumers of SD05 data: SD06 Snowflake receives all credit tables via CDC (MOD-042) for IFRS 9 portfolio-level ECL modelling (MOD-030, MOD-031), capital ratio calculations (MOD-033), and prudential reporting (MOD-036). SD08 back-office (MOD-074) reads credit applications and loan accounts for customer 360 views. Credit bureau submissions (MOD-059) write back to credit_bureau_requests from bank-credit.