Skip to content

Full systems & modules register — AI context

Generated: 2026-05-22 | 9 system domains | 177 modules

All system domain metadata, full module prose, policies satisfied, and build status on one page.


System index

ID System Repo Modules Build status
SD01 Core Banking Platform bank-core 20 Not started
SD02 Customer Identity & KYC Platform bank-kyc 9 Not started
SD03 AML Transaction Monitoring Platform bank-aml 4 Not started
SD04 Payments Processing Platform bank-payments 26 Not started
SD05 Credit Decisioning & Loan Platform bank-credit 17 Not started
SD06 Snowflake Analytics & Risk Platform bank-risk-platform 38 Not started
SD07 Data Platform & Governance Infrastructure bank-platform 27 Not started
SD08 Customer App & Back Office Platform bank-app 35 Not started
SD09 Brand & Public Surfaces bank-brand 1 Not started

Full system & module content


SD01 — Core Banking Platform

Repo: bank-core | Business domain: BD02 | Tech owner: Platform Engineering | Build status: Not started

The operational heart of the bank — real-time ledger, account management, interest engine, and transaction processing. Built on Postgres as the OLTP store with event streaming to Kafka.

Modules

ID Name Status ADR
MOD-001 Double-entry posting engine Not started ADR-001
MOD-002 Immutable transaction log Not started ADR-001
MOD-003 Real-time balance engine Not started ADR-001
MOD-004 Multi-currency ledger (NZD/AUD) Not started ADR-015
MOD-005 Daily accrual calculator Not started ADR-001
MOD-006 Rate change propagation Not started
MOD-007 Account state machine Not started
MOD-008 Dormancy & escheatment engine Not started

For full module specifications and acceptance criteria, see module specifications.

Architecture

See ADR-001 for the full Postgres-as-OLTP decision and the API + write-back architecture.

Critical constraints

  1. ledger_entries table is append-only — enforced by database trigger. Corrections via reversal entries only.
  2. Balance checks for payment authorisation must read from this system — never from Snowflake.
  3. Interest accrual must complete for all accounts by 23:59 daily (NFR-006).
  4. Multi-currency postings must be atomic — both legs of a NZD/AUD conversion post in a single transaction.

Modules in SD01


MOD-001 — Double-entry posting engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Enforces balanced debit/credit pairs on every transaction. Rejects any posting that does not balance to zero. Atomically posts both legs in a single Postgres transaction.

See Architecture Epic for acceptance criteria.

Policies satisfied:

Policy Mode Description
CLQ-006 — Capital Disclosure & Reporting Policy AUTO Every capital position is derived from posted ledger entries — no manual override path exists
REP-004 — Financial Statements Policy AUTO Statutory P&L and balance sheet sourced directly from ledger — no manual restatement
PAY-001 — Payment Operations Policy GATE Payment posting enforces settlement finality — entry is atomic and irreversible
CLQ-002 — Liquidity Risk Management Policy CALC Intraday liquidity position calculated from real-time ledger balances
PAY-007 — Ledger Posting & Account Integrity Policy LOG Every payment posting is recorded in the immutable ledger — provides the transaction record required for payment obligations and dispute resolution.
OPS-007 — Financial Processing Resilience & Idempotency Policy LOG Double-entry ledger creates an immutable audit trail for every financial transaction — provides the primary evidence base for operational risk event reconstruction.

MOD-002 — Immutable transaction log

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Every ledger entry is append-only. No record can be modified or deleted. Corrections are made via reversal entries, not edits. Full event-sourced history retained for 7 years.

Policies satisfied:

Policy Mode Description
GOV-006 — Internal Audit Policy LOG Internal audit has immutable access to all financial transactions — no data can be altered post-audit
REP-005 — Data Quality & Assurance Policy LOG Data lineage from source transaction to regulatory submission is fully traceable
AML-005 — Transaction Monitoring Policy LOG Transaction history for monitoring is immutable — cannot be suppressed or modified
PAY-002 — Settlement Risk Policy LOG Settlement records are permanent — provides legal certainty for all payment obligations
PAY-007 — Ledger Posting & Account Integrity Policy LOG Immutable transaction log provides the authoritative record of all payment events — cannot be altered or suppressed after posting.

MOD-003 — Real-time balance engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Customer and nostro balances updated synchronously on each ledger posting. Balance available via API within milliseconds of posting.

balance_updated event (v1.1.0)

MOD-003 consumes bank.core.posting_completed (from MOD-001) and re-emits bank.core.balance_updated v1.1.0 on every posting that affects a deposit account. The event carries the full before/after snapshot:

  • previous_ledger_balance + ledger_balance — balance before and after the posting
  • previous_available_balance + available_balance — available balance before and after
  • posting_id — the causal posting reference, used as the idempotency sequence
  • trace_id, correlation_id, jurisdiction, effective_at — context fields added in v1.1.0

No producer-side filtering is applied — all deposit account postings trigger an event. Consumers apply EventBridge detail-pattern filters for their specific use case (e.g. MOD-117 filters to accounts with an active overdraft facility).

The event is backwards-compatible with v1.0.0 consumers (MOD-070, MOD-077, MOD-032, MOD-042) — the new fields are additive. See event-catalogue.md §bank.core.balance_updated for the full field spec.

Policies satisfied:

Policy Mode Description
CLQ-002 — Liquidity Risk Management Policy CALC Liquidity monitoring uses live balances — no stale data risk in LCR calculation
PAY-001 — Payment Operations Policy GATE Sufficient funds check uses real-time balance before authorising any payment
CON-005 — Fee & Pricing Transparency Policy AUTO Customer-visible balance is always accurate and current — no lag between transaction and display
CLQ-004 — Interest Rate Risk in the Banking Book (IRRBB) Policy CALC IRRBB repricing model uses live rate-sensitive balance positions

MOD-004 — Multi-currency ledger (NZD/AUD)

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Maintains separate currency ledgers per account and per nostro. FX conversion entries routed through internal FX nostro pair. See ADR-015.

Policies satisfied:

Policy Mode Description
PAY-004 — Cross-Border Payments & FX Policy LOG Every NZD/AUD conversion recorded as matched pair through FX nostro — full audit trail
CLQ-001 — Capital Adequacy Policy CALC Capital ratios calculated against currency-adjusted RWA — multi-currency positions visible
AML-008 — Cross-Border Transfer Reporting Policy AUTO Cross-border transfer flag applied automatically on NZD↔AUD conversions for CMIR/IFTI reporting
REP-002 — Prudential Reporting Policy CALC Prudential returns include currency-split balance sheet sourced from ledger

MOD-005 — Daily accrual calculator

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Runs on each account at end of day. Applies the correct rate, day-count convention, and product rules. Posts accrual to GL without human intervention.

v2 — COB partitioning for portfolio scale

The current v1 implementation is a single-Lambda sequential pass across all interest-bearing accounts. This is correct and sufficient at early portfolio sizes. ADR-061 documents the partitioned execution model that v2 will adopt when the active account count exceeds the COB_PARTITION_THRESHOLD environment variable (default: 50,000 accounts).

How v2 works. A coordinator Lambda fans out the account population across N parallel Step Functions branches. Each branch processes one hash-bucketed partition of accounts. The coordinator waits for all branches to complete, then publishes bank.core.accrual_run_completed with the full run summary. The partition count is controlled by COB_PARTITION_COUNT (default: 10).

Trigger for v2 build. A CloudWatch alarm is configured to fire when the active account count exceeds COB_PARTITION_THRESHOLD for three consecutive nightly runs. That alarm is the signal to begin the v2 build. There is no manual action required before the alarm fires — v1 continues to operate correctly below the threshold.

Idempotency. Each partition posts accruals via MOD-001 using an idempotency key derived from (accrual_date, account_id). If a partition Lambda fails and Step Functions retries it, MOD-001 deduplicates on the key and the partition completes cleanly. No duplicate accrual entries are possible.

Same pattern for other portfolio-scale batch jobs. MOD-008 (dormancy assessment) and MOD-031 (ECL recalculation) will adopt the same partitioned Step Functions shape when they reach the equivalent scale threshold. The COB partition pattern in MOD-005 v2 is the reference implementation.

Design reference. Fineract's Close-of-Business engine (partitioned Spring Batch jobs across the loan portfolio) was studied as the battle-tested reference for this pattern. See fineract-design-reference.md.

Policies satisfied:

Policy Mode Description
REP-004 — Financial Statements Policy AUTO IFRS 9 interest income recognised on accrual basis — automated and auditable
CON-005 — Fee & Pricing Transparency Policy AUTO Interest earned displayed to customer reflects actual accrual — no rounding manipulation
CLQ-004 — Interest Rate Risk in the Banking Book (IRRBB) Policy CALC Rate-sensitive asset/liability positions updated automatically as accruals post
CRE-006 — Impairment & Provisioning Policy AUTO Effective interest rate method applied consistently across all loan accounts

MOD-006 — Rate change propagation

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

When a product rate is changed, the engine propagates the new rate to all affected accounts with the correct effective date and produces the regulatory disclosure trigger.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy AUTO Rate change applied to all affected accounts on correct effective date — no manual per-account update
CON-004 — Product Disclosure & Sales Practice Policy ALERT Rate change event triggers customer notification obligation flag — feeds comms engine
CLQ-004 — Interest Rate Risk in the Banking Book (IRRBB) Policy CALC IRRBB repricing gap updated automatically when rates change across the book

MOD-007 — Account state machine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Accounts move through defined states: Pending → Active → Restricted → Dormant → Closed. State transitions enforce regulatory rules — a Restricted account cannot originate payments.

State transition model

Transitions are evaluated by the pure transition-engine-pure.ts service, which enforces the following rules:

  • PENDING → ACTIVE: Standard path requires a matching row in accounts.kyc_status_mirror with status = 'VERIFIED'. Multi-party account types (trust, community, joint) bypass the single-party KYC mirror gate when the activation is initiated with an approved reason_code from the multi-party module (see below).
  • ACTIVE → RESTRICTED: Requires a restriction_reason from the allowed domain.
  • RESTRICTED → ACTIVE: Reinstatement — triggered by MOD-007 FR-440 reinstatement flow.
  • ACTIVE / RESTRICTED → DORMANT: Triggered when last_transaction_at crosses the dormancy threshold.
  • ANY → CLOSED: Terminal state.

ReasonCode domain

The ReasonCode union type in src/types/account-state.ts controls which callers are permitted to drive specific transitions. The following values are defined:

ReasonCode Used by Purpose
TRUST_GATE_PASS MOD-133 Bypass single-party KYC mirror gate on PENDING→ACTIVE for trust accounts; MOD-133 evaluateActivationGate already verified all trustee and BO identities
COMMUNITY_GATE_PASS MOD-134 Bypass single-party KYC mirror gate on PENDING→ACTIVE for community accounts; MOD-134 evaluateActivationGate verified all signatory identities
JOINT_GATE_PASS MOD-125 Bypass single-party KYC mirror gate on PENDING→ACTIVE for joint accounts; MOD-125 evaluateActivationGate verified all holder identities
SIGNATORY_KYC_DEGRADED MOD-134 ACTIVE→RESTRICTED transition when a community account's verified signatory count drops below the signing-rule minimum

The PENDING→ACTIVE pure validator bypasses the single-party KYC mirror gate when reason_code is TRUST_GATE_PASS, COMMUNITY_GATE_PASS, or JOINT_GATE_PASS. Cross-reference: MOD-133 §Activation gate, MOD-134 §Activation gate, MOD-125 §Activation gate.

RestrictionReason domain

The restriction_reason column CHECK on accounts.accounts (and in lockstep on accounts.account_state_history) accepts the following values:

Value Set by Trigger
SANCTIONS MOD-013 / compliance staff Sanctions match confirmed
FRAUD_INVESTIGATION Fraud operations Manual or automated fraud flag
HARDSHIP_ARRANGEMENT Customer operations Hardship arrangement in place
ADMIN Platform operations Administrative hold
INSUFFICIENT_SIGNATORIES MOD-134 Active verified signatory count drops below the community signing-rule minimum (FR-600). ACTIVE→RESTRICTED. Cleared when KYC is restored and check-signatory-kyc passes.

Note: Adding a new restriction_reason value requires extending the CHECK constraint on both accounts.accounts and accounts.account_state_history in the same migration (the transition engine writes both rows in the same Postgres transaction — see SD01 data model §DB-enforced invariants).

Hardship-flag service (V007)

V007 adds a party-level hardship flag store and four IAM-authenticated Function URL endpoints. The hardship flag is a mutable set/clear record on accounts.party_hardship_flags (one active row per party; see SD01 data model). It is intentionally mutable — the flag is cleared when the hardship arrangement ends, and a party may be re-flagged if a subsequent hardship arrangement is opened.

Endpoints

All four endpoints use AuthType=AWS_IAM. Access is controlled by the broader bank-platform IAM layer; no per-Principal resource policies are applied.

Endpoint Method Description Response codes
hardship-flag-set-url POST Sets the hardship flag for a party. Body: { party_id, flagged_by_module, reason }. Idempotent — 200 on first set; 409 HARDSHIP_FLAG_ALREADY_SET if already flagged. Re-flags a previously cleared party (inserts new row). 200, 409
hardship-flag-clear-url DELETE Clears the active hardship flag. Body: { party_id, cleared_by_module }. 200, 404
hardship-flag-read-url GET Returns { flagged: bool, flagged_at?, flagged_by_module?, reason? }. 200
primary-deposit-account-url GET Returns { account_id } for the party's most-recently-opened ACTIVE deposit account (NZ_TRANSACTION_01 / AU_TRANSACTION_01 / NZ_SAVINGS_01 / AU_SAVINGS_01). 200, 404

SSM output paths

/bank/{env}/mod-007/hardship-flag-set-url
/bank/{env}/mod-007/hardship-flag-clear-url
/bank/{env}/mod-007/hardship-flag-read-url
/bank/{env}/mod-007/primary-deposit-account-url
/bank/{env}/mod-007/party-hardship-flags-table   → "accounts.party_hardship_flags"

Callers

  • MOD-116 (bank-credit) — Day-7 arrears path POSTs to hardship-flag-set-url; discharge handler GETs primary-deposit-account-url.
  • MOD-117 (bank-credit) — On consecutive_drawn_days ≥ 60, POSTs to hardship-flag-set-url with flagged_by_module='MOD-117'.
  • MOD-065 (bank-credit) — On HARDSHIP_RESOLUTION case closure, DELETEs via hardship-flag-clear-url.

Design note: soft FK to SD02

accounts.party_hardship_flags.party_id references party.parties in SD02's bank_kyc DB. Cross-DB FKs are not supported in this Neon deployment (same pattern as V005 account_party_relationships). The column accepts the SD02 UUID identifiers; referential integrity is enforced at the service layer.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy GATE Account cannot be activated until KYC status is Verified — GATE enforced at state machine level
AML-007 — Sanctions Screening Policy GATE Account is automatically restricted if sanctions match is confirmed — no agent override without approval
PAY-005 — Payment Fraud Prevention Policy GATE Fraud-flagged account automatically restricted pending investigation

MOD-008 — Dormancy & escheatment engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Identifies accounts with no customer-initiated transactions for the statutory period (7 years NZ/AU). Flags for customer contact, then processes transfer to unclaimed money register.

Policies satisfied:

Policy Mode Description
CON-001 — Customer Fairness & Conduct Policy AUTO Dormant customer contacted before funds transferred — fair conduct obligation met automatically
REP-001 — Regulatory Reporting Policy AUTO Unclaimed money return filed with IRD (NZ) / ASIC (AU) automatically on schedule

MOD-110 — Fee engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

What it does

MOD-110 evaluates, assesses, waives, and posts fees for all products on the platform. It is the single point of truth for fee logic — no module posts fees directly to MOD-001; all fee postings flow through MOD-110. This ensures consistent fee audit trails, correct waiver evaluation, and compliant advance notification before any new fee type or rate change takes effect.

In a SaaS context, the fee schedule is tenant-configurable: each institution on the platform defines its own fee types, amounts, waiver conditions, and notice periods via the fee schedule configuration table. MOD-110 evaluates those rules; it does not hardcode any amounts.

Fee types supported

Monthly account fee, transaction fee (per debit/credit above threshold), dishonour/return fee (for failed direct debits or returned payments), overlimit fee, late payment fee (for credit products), break cost fee (for term deposits and fixed-rate loans — computed by MOD-111 and MOD-112 respectively, posted by MOD-110), early repayment fee, paper statement fee.

Waiver conditions

Each fee type supports configurable waiver conditions evaluated at assessment time: zero balance (do not charge a fee against an account that has no funds), negative balance (account already in debit), recently opened (waive for first N months post-account opening), waiver flag on the account record (one-time or standing waiver applied by an agent), promotional period (product-level promotional window). The waiver evaluation result is recorded in posting metadata whether or not a waiver is applied, enabling audit reconstruction without replaying business logic.

Staff/employee rate waiver and promotional waiver codes via agent deal (MOD-109) are deferred to v2.

Advance notice gate

When a fee schedule change is published (new fee type or rate increase), the engine computes the operative effective date as MAX(proposed_effective_from, published_at + notice_days). notice_days defaults to 14 and is subject to a minimum of 14 for all retail products (CON-005 floor — not a configurable option). The GATE prevents posting of any fee under the new schedule until the computed effective date is reached. Fee reductions take effect at proposed_effective_from immediately, without a notice gate. Same-rate re-publications still write a new schedule row with updated metadata for the audit trail; no gate is applied. The response for any assessed fee that has been gated to a future date must surface the effective_from date in the response body.

Reversal

Any fee may be reversed by an authorised agent via MOD-083. In v1, reversals are record-only — no approval tier is validated. The reversal is posted as a compensating credit entry via MOD-001 and logged to the fee audit trail with the authorising staff_id and reason. Approval tier gating (configurable thresholds per fee type and amount) is deferred to the MOD-109 integration in v2.

Schedule administration

v1: fee schedules are seeded via the V005 migration (psql) only. No admin API ships in v1. When MOD-006 (product catalogue) delivers its admin API, MOD-110 will subscribe to the bank.core.fee_schedule_published event to apply changes at runtime.

Cron-driven fees

Monthly recurring fees (account maintenance, card fees) are out of scope for v1. MOD-110 is an HTTP-triggered service only in v1. Recurring fee scheduling is deferred until the use case is confirmed by product and a reliable cron + idempotency mechanism is in place.

Idempotency

All fee assessment requests must supply a caller-generated idempotency key. The key is stored under a UNIQUE constraint on core.fee_events. On duplicate key, the engine returns 200 with the original response body — not 409. Callers (event handlers, API gateway integrations) are responsible for generating and durably storing the key before calling the engine.

Currency validation

If the account's currency does not match the fee schedule's currency, the engine returns CURRENCY_MISMATCH 422. This is a non-retryable client error validated in the pre-flight block, before any posting attempt.

Data model

-- core.fee_schedule (Postgres — tenant-configurable)
CREATE TABLE core.fee_schedule (
  schedule_id       uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id         text NOT NULL,
  product_id        text NOT NULL,
  fee_type          text NOT NULL,
  amount            numeric(18,2) NOT NULL,
  currency          text NOT NULL,
  waiver_conditions jsonb,              -- array of condition objects
  notice_days       int NOT NULL DEFAULT 14,
  effective_from    date NOT NULL,
  effective_to      date,
  version           int NOT NULL,
  published_at      timestamptz NOT NULL DEFAULT now()
);

-- core.fee_events (append-only)
CREATE TABLE core.fee_events (
  event_id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id        uuid NOT NULL,
  party_id          uuid NOT NULL,
  fee_type          text NOT NULL,
  assessed_amount   numeric(18,2) NOT NULL,
  posted_amount     numeric(18,2),        -- null if waived
  waived            boolean NOT NULL DEFAULT false,
  waiver_reason     text,
  waiver_check      jsonb,               -- {"evaluated": [...], "applied": "condition_name" | null}
  schedule_version  int NOT NULL,
  posting_id        uuid,                -- references core.postings(id) if posted
  reversal_of       uuid REFERENCES core.fee_events(event_id),
  jurisdiction      text NOT NULL,
  idempotency_key   text NOT NULL,
  assessed_at       timestamptz NOT NULL DEFAULT now(),
  UNIQUE (idempotency_key)
);

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy GATE Fee posting is blocked if the required advance notice period has not elapsed since the fee schedule was last changed — enforcing the notification-before-deduction obligation.
CON-004 — Product Disclosure & Sales Practice Policy LOG Every fee event (assessment, waiver, posting, reversal) is logged immutably with the fee type, amount, waiver reason if applicable, and applicable fee schedule version.
PAY-001 — Payment Operations Policy AUTO Fees are posted as double-entry ledger entries via MOD-001 — fee debit from the customer account, credit to the bank's fee income GL account — in a single atomic transaction.

MOD-111 — Term deposit maturity engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

What it does

MOD-111 manages the full maturity lifecycle of term deposit accounts — from pre-maturity notification through to maturity proceeds disbursement or early exit. It handles standing instructions capture, auto-rollover execution, break cost calculation, and the disclosure gate that prevents early withdrawal without explicit cost acceptance.

Pre-maturity notification

A daily batch identifies term deposits maturing within 30, 14, and 7 calendar days. Notification events are published to MOD-063 (notification orchestration) at each threshold, with: product name, maturity date, current balance, projected maturity proceeds (principal + accrued interest), current rollover rate for the same term (to assist the customer's decision). The first notification at 30 days includes the standing instruction form — the customer can set: rollover to same term, rollover to different term, withdraw all to nominated account, partial rollover + partial withdrawal.

Maturity instructions

Instructions are stored against the account with a confirmation timestamp. If no instruction is received by 23:59 two business days before maturity, the system auto-applies the account's default instruction (configured at account opening — typically auto-rollover to same term at prevailing rate). Customers can change their instruction up to one business day before maturity.

Auto-rollover execution

On maturity date, a batch job: (1) calculates final accrued interest via MOD-005, (2) posts interest credit via MOD-001, (3) if rollover — re-fixes the balance at the new term and rate, updates the maturity date, posts a rollover event; if withdrawal — disburses proceeds to the nominated account via MOD-020/MOD-001. All steps are idempotent — replayable without double-posting.

Break cost calculation

Break cost = (Contract Rate − Current Reinvestment Rate) × Outstanding Balance × (Days Remaining / 365)

Where:
  Contract Rate             = the fixed rate at which the deposit was opened
  Current Reinvestment Rate = prevailing rate for the remaining term (sourced from MOD-006 rate register)
  If (Contract Rate − Current Reinvestment Rate) ≤ 0: break cost = 0 (rate has risen; no penalty to customer)

Break cost is never negative — it is a cost to the customer if rates have fallen, zero if rates have risen. The calculation is disclosed to the customer before any early withdrawal proceeds. The customer must explicitly accept via app confirmation or agent confirmation before funds are released.

Data model

-- core.term_deposit_instructions (Postgres)
CREATE TABLE core.term_deposit_instructions (
  instruction_id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id        uuid NOT NULL,
  party_id          uuid NOT NULL,
  instruction_type  text NOT NULL CHECK (instruction_type IN ('ROLLOVER_SAME','ROLLOVER_DIFFERENT','WITHDRAW_ALL','PARTIAL_ROLLOVER')),
  rollover_term_days int,              -- null unless ROLLOVER_DIFFERENT or PARTIAL_ROLLOVER
  withdrawal_amount  numeric(18,2),   -- null unless PARTIAL_ROLLOVER
  nominated_account  uuid,            -- destination for withdrawal proceeds
  captured_at        timestamptz NOT NULL DEFAULT now(),
  source             text NOT NULL    -- 'customer_app' | 'agent' | 'auto_default'
);

-- core.break_cost_disclosures (append-only — consent trail)
CREATE TABLE core.break_cost_disclosures (
  disclosure_id     uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id        uuid NOT NULL,
  party_id          uuid NOT NULL,
  break_cost_amount numeric(18,2) NOT NULL,
  contract_rate     numeric(8,6) NOT NULL,
  reinvestment_rate numeric(8,6) NOT NULL,
  days_remaining    int NOT NULL,
  disclosed_at      timestamptz NOT NULL DEFAULT now(),
  accepted_at       timestamptz,       -- null until customer accepts
  accepted_via      text               -- 'app' | 'agent'
);

Policies satisfied:

Policy Mode Description
CON-004 — Product Disclosure & Sales Practice Policy AUTO Pre-maturity notifications are sent automatically at 30, 14, and 7 days before maturity — no manual trigger required, no customer is missed.
CON-005 — Fee & Pricing Transparency Policy GATE Early withdrawal is blocked until the break cost is calculated, disclosed to the customer, and explicitly accepted — no funds are released until acceptance is recorded.
PAY-001 — Payment Operations Policy AUTO Maturity proceeds are disbursed automatically on the maturity date per the customer's standing instruction, with no manual intervention required.

MOD-112 — Amortisation schedule engine

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

What it does

MOD-112 generates and maintains the amortisation schedule for all instalment loan products. It computes the initial schedule at origination, serves the schedule to the customer, and recalculates it whenever a schedule-changing event occurs — variable rate change, extra repayment, interest-only period expiry, or hardship restructure.

Schedule generation

At loan origination, the module produces a full schedule of payment dates, payment amounts, principal components, interest components, and running outstanding balance. The schedule is generated using the declining balance method. For variable rate loans, the initial schedule uses the origination rate with a note that instalments will adjust when the rate changes. For fixed rate loans, the schedule is deterministic for the full term.

Repayment amount formula:

P = L × [r(1+r)^n] / [(1+r)^n − 1]

Where:
  P = periodic repayment amount
  L = loan principal
  r = periodic interest rate (annual rate / payment frequency periods per year)
  n = total number of payment periods

Schedule events that trigger recalculation

  1. Variable rate change (MOD-006 event received) — new instalment amount calculated from outstanding balance, remaining term, new rate; schedule regenerated from next payment date.
  2. Extra repayment — customer makes a payment above the scheduled amount; module offers two options: reduce term (same instalment, shorter loan) or reduce instalment (same term, lower payment). Customer selects via app; new schedule generated and delivered.
  3. Interest-only period expiry — schedule transitions from interest-only payments to principal and interest; instalment amount recalculates over remaining P&I term.
  4. Hardship restructure (from MOD-065) — new schedule generated from restructured terms; original schedule retained for audit; both stored as separate schedule versions.
  5. Loan variation (from MOD-132) — new schedule generated from confirmed variation terms (new rate, term, frequency, rate type); prior is_current=true schedule superseded. See POST /internal/v1/loans/{account_id}/recalc-variation below.

HTTP surface

Endpoint Purpose
POST /internal/v1/loans/{account_id}/originate-schedule Initial schedule at loan origination — origination only
POST /internal/v1/loans/{account_id}/recalc-rate Recalculate from new variable rate
POST /internal/v1/loans/{account_id}/extra-repayment Stage an extra-repayment recalculation
POST /internal/v1/loans/{account_id}/extra-repayment/accept Confirm extra-repayment option
POST /internal/v1/loans/{account_id}/recalc-variation Recalculate from confirmed loan variation (MOD-132, FR-590)
GET /internal/v1/loans/{account_id}/schedule Retrieve current schedule
GET /internal/v1/loans/{account_id}/total-cost Total cost of credit

recalc-variation request body (RecalcVariationRequestV1@bank-core/mod-112-contracts@1.1.0)

{
  "new_annual_rate":       "0.0625",
  "new_term_months":       84,
  "new_payment_frequency": "MONTHLY | FORTNIGHTLY | WEEKLY",
  "new_rate_type":         "FIXED | VARIABLE",
  "variation_id":          "uuid (MOD-132 correlation)",
  "effective_date":        "YYYY-MM-DD",
  "idempotency_key":       "≥ 8 chars"
}

Produces a new credit.amortisation_schedules row with generated_by = 'restructure', version = current.version + 1, is_current = true; supersedes the prior current schedule. Caller-supplied idempotency_key — replay returns null (no double-recalc). Publishes bank.core.amortisation_schedule_recalculated with generated_by = 'restructure' and variation_id in metadata.

SSM paths

Path Value
/bank/{stage}/mod-112/api/base-url Internal API base URL
/bank/{stage}/mod-112/variation-recalc-api Full URL for recalc-variation endpoint (MOD-132 reads this directly)
/bank/{stage}/mod-112/lambdas/recalc-variation/arn Lambda ARN for the recalc-variation handler

Minimum repayment (revolving credit)

For revolving credit facilities (PRD-008), the minimum monthly repayment is:

min_repayment = max(configured_floor_amount, outstanding_balance × configured_percentage)

Both values are tenant-configurable in the fee schedule. The minimum repayment is posted automatically on the due date if no payment has been received; if a payment has been received but is below the minimum, the shortfall triggers a late payment fee assessment via MOD-110.

Data model

Edge case coverage — Fineract LoanScheduleGenerator comparison

As part of the Fineract design reference review (see fineract-design-reference.md), MOD-112 was compared against Fineract's LoanScheduleGenerator and RepaymentScheduleInstallment logic. Fineract's implementation is the most battle-tested open-source reference for these patterns, having been exercised across portfolios of millions of accounts in 80+ countries. The comparison identified the following coverage map and v2 scope items.

Covered in v1

Scenario MOD-112 v1 status
Standard P&I declining balance schedule ✓ Full
Variable rate change — recalculate from outstanding balance ✓ Full
Fixed rate — deterministic schedule for full term ✓ Full
Extra repayment: reduce-term option ✓ Full
Extra repayment: reduce-instalment option ✓ Full
Interest-only period transitioning to P&I ✓ Full
Hardship restructure (new terms, versioned schedule) ✓ Full
Revolving minimum repayment calculation ✓ Full

Not covered in v1 — v2 scope items

Balloon payment structures. Some mortgage and business loan products have a large final payment (balloon) that is materially larger than the regular instalment. The current formula produces equal instalments; it does not support a product parameter that sets a balloon amount and back-calculates the regular instalments accordingly. Fineract handles this via loanProductData.amortizationType = EQUAL_INSTALLMENTS vs BALLOON. Flag as v2 when a product requiring a balloon structure is launched.

Payment holiday (deferral period). A customer may be granted a payment holiday — a period during which no instalment is due but interest continues to accrue and is capitalised onto the principal. The v1 implementation does not model this; hardship restructure is the closest equivalent but requires new terms rather than a deferral window. Fineract's isNpa and payment-defer logic is the reference. Flag as v2 when hardship deferral (as distinct from full restructure) is introduced.

Capitalisation of arrears balance. In some products, unpaid interest in arrears is capitalised onto principal (added to the outstanding balance) at a configurable frequency rather than being tracked as a separate overdue amount. v1 tracks arrears separately via MOD-065. Fineract supports compoundingMethod = INTEREST with periodic capitalisation. Flag as v2 if a product requiring arrears capitalisation is launched.

Day-count precision at boundary conditions. v1 uses actual/365 throughout. Fineract supports actual/actual, actual/360, 30/360, and actual/365. For NZ mortgage products, CCCFA disclosure requirements specify the exact day-count convention, and some products may require actual/actual (which produces slightly different amortisation in leap years and at month boundaries). Review required before a product with a non-actual/365 day-count convention is launched.

Early full settlement — interest rebate calculation. When a customer repays a fixed-rate loan in full before the maturity date, a Rule of 78 or actuarial rebate of prepaid interest may apply depending on the product terms. v1 closes the schedule via MOD-065 but does not compute an interest rebate. Fineract's InterestRefundService is the reference implementation. Flag as v2 when a fixed-rate product with early settlement rebate is required.

-- credit.amortisation_schedules (Postgres)
CREATE TABLE credit.amortisation_schedules (
  schedule_id        uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         uuid NOT NULL,
  version            int NOT NULL DEFAULT 1,
  schedule_type      text NOT NULL CHECK (schedule_type IN ('PI','IO','REVOLVING')),
  generated_at       timestamptz NOT NULL DEFAULT now(),
  generated_by       text NOT NULL,   -- 'origination' | 'rate_change' | 'extra_repayment' | 'restructure'
  rate_at_generation numeric(8,6) NOT NULL,
  is_current         boolean NOT NULL DEFAULT true
);

CREATE TABLE credit.schedule_instalments (
  instalment_id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  schedule_id      uuid NOT NULL REFERENCES credit.amortisation_schedules(schedule_id),
  payment_number   int NOT NULL,
  due_date         date NOT NULL,
  payment_amount   numeric(18,2) NOT NULL,
  principal_amount numeric(18,2) NOT NULL,
  interest_amount  numeric(18,2) NOT NULL,
  opening_balance  numeric(18,2) NOT NULL,
  closing_balance  numeric(18,2) NOT NULL,
  status           text NOT NULL DEFAULT 'PENDING'   -- PENDING | PAID | MISSED | PARTIAL
);

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy AUTO Full amortisation schedule is generated and provided to the customer at loan origination — disclosure of total cost of credit, total interest payable, and repayment schedule is automated with no manual step.
CON-004 — Product Disclosure & Sales Practice Policy AUTO Any event that changes the repayment schedule (rate change, extra repayment, hardship restructure) triggers an automatic updated schedule delivery to the customer within 24 hours.
CON-005 — Fee & Pricing Transparency Policy CALC Total interest payable and effective annual rate are computed from the schedule and displayed on the product dashboard at all times.

MOD-118 — Member equity and share registry

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Maintains the definitive record of member shareholdings for mutual institutions (building societies, credit unions, mutual banks). Records all share transactions — purchase, redemption, transfer, dividend reinvestment, and correction — manages the dividend declaration and distribution workflow, and enforces the capital-gate on share redemptions. This module only activates when the tenant's institution type is configured as mutual.

Configuration flag

The module is governed by a tenant-level flag at SSM path /bank/{stage}/tenant/institution-type (values: mutual | proprietary). The Lambda reads this at cold-start. When proprietary, all MOD-118 handlers return immediately (404 or 501). This allows the same platform to serve both company-structured banks and mutual institutions without separate deployments. The flag is set at tenant provisioning time and is not changeable at runtime.

Compliance rationale

Mutual institutions have regulatory obligations that differ materially from proprietary banks. Member share capital may qualify as Common Equity Tier 1 (CET1) capital under Basel III/IV, but only if redemption is at the full discretion of the institution — i.e. the institution can block redemption without member consent. This module implements that discretionary block as a hard gate: no redemption proceeds until MOD-033 confirms that the post-redemption CET1 ratio would remain at or above the regulatory minimum.

Failure to implement this gate would result in member shares being classified as a liability (AT1 at best) rather than CET1, which would materially reduce reported capital ratios and could trigger prudential intervention by the RBNZ or APRA. The gate is therefore a regulatory compliance requirement, not an operational feature.

In NZ, the relevant standards are RBNZ BS2A and BS2B. In AU, the relevant standard is APRA APS 110. Both require the same underlying control.

Commercial rationale

Building societies and credit unions use member equity as both a regulatory capital tool and a customer loyalty mechanism. Membership confers voting rights, access to surplus distributions, and a sense of institutional ownership that differentiates mutual institutions from proprietary banks. A well-managed dividend workflow — with transparent calculation, automatic payment, and clear tax documentation — is a core element of the mutual institution brand proposition and directly supports member retention.

Data model

Five tables in the core schema. Full schemas in SD01 data model. Key design points:

  • party_id not customer_idcore.member_register.party_id is UUID NOT NULL with no Postgres FK (cross-domain boundary; mirrors the pattern in accounts.account_party_relationships). core.customers does not exist in bank-core's DB.
  • Uppercase enums — all CHECK enum values are uppercase per SD04 standard.
  • core.share_transactions is Cat 1 immutable — INSERT only; each share capital movement is one row in terminal state. Status transitions are recorded as new rows (CORRECTION tx_type for reversals), not updates.
  • core.redemption_queue — mutable FIFO queue for redemptions blocked by the capital gate. Replayer processes this daily in FIFO order when headroom allows.

See SD01 data model for full column definitions.

Key operations

Share purchase

Validate that the party holds member or applicant status in MOD-007 (via accounts.account_state_history). Eligibility rule evaluation (MOD-105) is deferred to v2 — v1 accepts any status='member' party (Gap #4 ruling). Process payment for the share purchase amount (par value × number of shares). Call MOD-001 to post the debit to the member's payment account and a credit to equity.share_capital. Insert a row in core.share_transactions with tx_type = 'purchase' and update shares_held on the member register. If this is the initial share purchase (first-time member), set status to member via MOD-007. Issue membership certificate via MOD-073, storing the document reference on the member register.

Redemption gate

CapitalGateProvider interface. The capital gate is implemented behind a CapitalGateProvider interface (src/lib/capital-gate-provider.ts). v1 ships with MOD033CapitalGateProvider (reads GET /capital/current/{jurisdiction} on the MOD-033 Lambda wrapper — see Gap #1). Dev/uat uses a StubCapitalGateProvider (always returns a healthy ratio) so the build is not blocked pending the MOD-033 wrapper. The interface is the same pattern as MOD-087's EnrichmentProvider — swapping implementations is mechanical.

Before processing any redemption request: invoke CapitalGateProvider.getCET1Ratio(jurisdiction). Compute the post-redemption CET1: (current_tier1_capital − redemption_amount) / current_rwa. If the result would fall below the configured floor (/bank/{stage}/tenant/cet1-minimum), insert a BLOCKED row in core.share_transactions and a corresponding row in core.redemption_queue. Notify the member via bank.core.member_status_changed event (MOD-063 delivers). The blocked-redemption-replayer runs daily: evaluates FIFO queue, calls CapitalGateProvider for current ratio, processes queued redemptions when headroom allows. This gate cannot be bypassed by any back-office role.

Dividend declaration

Board-initiated via the back-office administration interface. Input parameters: record date, payment date, rate per share, board resolution reference. The system calculates total declared amount from the current member register as SUM(shares_held) × rate_per_share at the record date snapshot. A dividend declaration record is created with status = 'declared'. MOD-001 posts the journal entry: debit equity.retained_earnings, credit liabilities.dividend_payable.

On the payment date, a batch job iterates all active members as at the record date. For each member: calculate gross_dividend = shares_at_record × rate_per_share; apply applicable withholding tax per jurisdiction; post net credit to the member's nominated account via MOD-001; insert a row in core.dividend_payments with paid_at set to the payment timestamp. Update declaration status to PAID when all payments are settled. At financial year end, MOD-118 publishes a bank.core.tax_certificate_due event per member who received a dividend during the year, carrying the full calculation payload. MOD-113 subscribes to this event when deployed and renders the tax certificate before storing in MOD-073. FR-535 certificate delivery is deferred until MOD-113 is active (Gap #2 ruling — Option C).

AGM voting record

When a member casts a vote at the AGM (via the app or in-person verification), update last_voted_at on the member register to the AGM date. This field is included in the annual member statement generated by MOD-113. Voting eligibility is determined by status = 'member' as at the AGM record date.

Requirements satisfied

FR-533 covers share register management — creation, update, and audit trail of member shareholding records. FR-534 covers capital-gate enforcement on redemptions — the requirement that no redemption proceeds if it would breach the minimum CET1 ratio. FR-535 covers dividend declaration and distribution workflow — board declaration, per-member calculation, ledger posting, and payment. FR-536 covers tax certificate generation — annual dividend tax certificate per member, integrated with MOD-113.

Policies satisfied:

Policy Mode Description
CLQ-001 — Capital Adequacy Policy GATE Share redemption requests that would reduce CET1 capital below the regulatory minimum are blocked — the redemption queue is held until capital headroom is restored.
GOV-001 — Board Charter LOG All share transactions, dividend declarations, and member register changes are logged as immutable governance events for board and regulatory review.
CON-001 — Customer Fairness & Conduct Policy AUTO Dividend distributions are calculated consistently across all members using the declared rate and recorded shareholding — no discretionary variation.
CON-004 — Product Disclosure & Sales Practice Policy AUTO Members receive their annual member statement and dividend tax certificate automatically without requiring a manual request.

MOD-125 — Joint account management

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Manages multi-party account ownership for joint accounts. Maintains the relationship between an account and each of its named holders, including signing authority configuration, individual KYC tracking per holder, balance apportionment for regulatory reporting (NZ DCS Single Depositor View), and the lifecycle events unique to joint accounts: holder death, holder removal, and holder addition.

Regulatory dimension — NZ Deposit Takers Act 2023 / Depositor Compensation Scheme

The NZ Depositor Compensation Scheme (DCS), live 1 July 2025, requires every NZ-licensed deposit taker to maintain a Single Depositor View (SDV) — a data file that aggregates each natural person's total insured deposits across all accounts. Joint accounts are protected per holder up to $100,000 per person.

The SDV file must apportion the joint account balance equally across all named holders unless the account agreement specifies otherwise. This means:

  • Every holder must be individually identified as a natural person, not recorded as a single "joint account" entity.
  • Each holder's share of the account balance must be calculable at any point in time.
  • REP-007 must be able to sum each person's total deposits including their proportional share of all joint accounts they hold.

This is a prudential compliance requirement under the DTA, not solely an AML obligation. Non-compliance with the SDV requirement is a reportable breach to the Reserve Bank of NZ.

Signing authority model

Three modes are supported at the account level:

  • any_one — any single holder can initiate and confirm transactions. Used for most personal joint accounts.
  • all — all holders must authorise each transaction. Used for business and trust accounts with multiple required signatories.
  • any_two — any two of N holders must authorise. Used for larger groups where all would be operationally impractical.

The signing authority mode is set at account opening and can be amended only with consent of all active holders. Changes are logged to joint_account_events.

Data model

-- core.account_holders
CREATE TABLE core.account_holders (
  holder_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  customer_id        UUID NOT NULL REFERENCES core.customers(customer_id),
  holder_type        TEXT NOT NULL CHECK (holder_type IN ('primary','joint','minor','trustee')),
  balance_share_pct  NUMERIC(5,2) NOT NULL DEFAULT 50.00,  -- for DCS/SDV apportionment
  signing_authority  TEXT NOT NULL DEFAULT 'any_one' CHECK (signing_authority IN ('any_one','all','any_two')),
  kyc_status         TEXT NOT NULL DEFAULT 'pending' CHECK (kyc_status IN ('pending','verified','failed')),
  consent_given      BOOLEAN NOT NULL DEFAULT false,
  consent_given_at   TIMESTAMPTZ,
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','deceased','removed')),
  added_at           TIMESTAMPTZ NOT NULL DEFAULT now(),
  removed_at         TIMESTAMPTZ
);

-- core.joint_account_events
CREATE TABLE core.joint_account_events (
  event_id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  event_type         TEXT NOT NULL CHECK (event_type IN ('holder_added','holder_removed','holder_deceased','authority_changed','share_adjusted','account_closed')),
  initiated_by       UUID,  -- customer_id of the initiating holder or NULL for system events
  event_data         JSONB,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

balance_share_pct values across all active holders for a given account must sum to 100.00. The application layer enforces this constraint; the database constraint enforces it per row.

Key operations

Joint account opening

The primary holder initiates account opening. Invited holder(s) receive a secure link via MOD-063 to complete their portion of the application. Each holder completes eIDV via MOD-009 independently — there is no shared or delegated identity step.

MOD-007 holds the account in pending_holders state until all required holders have:

  1. Completed eIDV and received a KYC status of verified.
  2. Provided individual consent via MOD-049.

The account activates to active only when all conditions are met across all holders. Invited holders have 14 days to complete. After that deadline, the primary holder is notified via MOD-063 and may re-issue the invitation.

Transaction authorisation

For any_one accounts: any holder's authenticated session can initiate and confirm transactions through the standard payment flow.

For all accounts: payment initiation creates a pending_authorisation record. All other holders receive a notification via MOD-063 and must approve via their own biometric session. The payment submits only when all required approvals are received. Requests expire after 24 hours if not fully approved; the initiating holder is notified on expiry.

For any_two accounts: the same pending authorisation flow applies, but the threshold is met when any two distinct holders have approved.

Death of a holder

A holder's death is notified by the estate or by a surviving holder. The module sets the deceased holder's status = deceased and records the notification date in joint_account_events. The account continues operating under the surviving holders' existing signing authority.

The deceased holder's balance_share_pct is frozen for estate administration. Surviving holders cannot transfer or redistribute the deceased holder's share until the estate provides legal documentation (death certificate and probate or letters of administration, stored via MOD-073). Once documentation is accepted by back-office review, the share can be redistributed or paid out to the estate.

DCS / SDV apportionment

balance_share_pct is set at account opening. The default for a two-holder account is 50.00% each. For SDV purposes, the holder's insured deposit contribution from this account is:

holder_insured_amount = account_balance × (balance_share_pct / 100)

REP-007 queries core.account_holders to aggregate each customer_id's total insured deposits across all accounts, including joint account shares. Non-default splits require written agreement from all holders at opening; the agreement is stored in MOD-073 and event_data on the share_adjusted event records the rationale.

Holder removal

Any active holder may request to be removed from a joint account. Removal requires consent of all remaining holders (or a court order). The module sets status = removed and removed_at for the departing holder, recalculates and distributes the departing holder's balance_share_pct equally among remaining holders (or as agreed), and logs a holder_removed event to joint_account_events. The account remains open for the remaining holders.

Holder addition

An existing holder may propose adding a new holder. All current active holders must consent. The new holder completes eIDV via MOD-009 and provides individual consent before they are activated. balance_share_pct values are renegotiated across all holders at the time of addition.

Requirements

FR-565 — Multi-party account activation: account must not activate until all required holders have individually completed eIDV and provided consent.

FR-566 — Signing authority enforcement: transaction authorisation flow must enforce the account's signing_authority mode; all and any_two modes must block submission until the required number of distinct holder approvals is recorded.

FR-567 — DCS SDV apportionment: balance_share_pct must be maintained per holder and queryable by REP-007 at any point in time; the sum of active holders' shares must equal 100.00.

FR-568 — Holder death workflow: on notification of a holder's death, the module must freeze the deceased holder's share and prevent redistribution until valid legal documentation is recorded in MOD-073.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy GATE All joint account holders must individually pass eIDV and CDD tier assignment before the account activates — partial KYC completion does not allow the account to open.
CON-001 — Customer Fairness & Conduct Policy AUTO All joint account holders receive statements, notices, and communications simultaneously — no holder receives less information than any other.
PRI-001 — Privacy Policy AUTO Each joint account holder's personal data is processed with individual consent; each holder has individual rights of access, correction, and portability over their own data.
REP-007 — DCS & Depositor Reporting Policy CALC Each joint account holder's proportional share of the account balance is calculated and available for the Single Depositor View (SDV) file required under the NZ Depositor Compensation Scheme (DTA 2023).

MOD-130 — Notice account management

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Manages the notice period lifecycle for notice account products (PRD-021). Records notice lodgements, calculates withdrawal dates, enforces the notice gate (prevents withdrawal before the date arrives), calculates and discloses early withdrawal penalties, and auto-executes withdrawals on the notice expiry date if a standing instruction exists.

Compliance rationale

Notice accounts are a liquidity management tool: the bank uses the notice period to plan funding. CLQ-002 (Liquidity Risk Management Policy) requires the platform to correctly classify notice deposits as non-callable within the notice window when computing the LCR/NSFR (MOD-032). Misclassifying notice deposits as at-call would overstate short-term liquidity and could result in a regulatory breach of minimum liquidity requirements.

The early withdrawal penalty exists to deter customers from treating the notice account as at-call. Its disclosure requirement under CON-005 must be upfront and unambiguous — the module enforces this via a hard disclosure gate before any early withdrawal can proceed.

Commercial rationale

Notice accounts fill the gap between at-call savings (highest liquidity, lowest rate) and term deposits (lowest liquidity, highest rate). They are a common building society product and a key tool for institutions managing their funding mix toward a more stable base without locking customers into long fixed terms. Offering a compelling notice rate allows the bank to attract longer-duration savings while maintaining a predictable withdrawal schedule.

Data model

-- core.notice_lodgements
CREATE TABLE core.notice_lodgements (
  lodgement_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  notice_period_days INT NOT NULL,
  lodged_at          TIMESTAMPTZ NOT NULL,
  withdrawal_date    DATE NOT NULL,  -- lodged_at + notice_period_days
  amount             NUMERIC(18,2),  -- null = full balance
  status             TEXT NOT NULL DEFAULT 'pending'
                       CHECK (status IN ('pending','withdrawn','cancelled','lapsed')),
  cancelled_at       TIMESTAMPTZ,
  penalty_amount     NUMERIC(18,2),
  withdrawn_at       TIMESTAMPTZ,
  posting_id         UUID,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

Key operations

1. Lodge notice

Customer submits a withdrawal notice via the app or API. The module validates that the account status is active and that no other active notice exists for the same amount. It calculates withdrawal_date = CURRENT_DATE + notice_period_days and creates the lodgement record. MOD-007 transitions the account to notice_pending. MOD-063 dispatches a confirmation notification: "Your notice has been received. Funds will be available on [date]."

2. Enforce notice gate

Any withdrawal attempt against an account in notice_pending status is blocked with a clear error response. The response includes the upcoming withdrawal date and the pending lodgement details. No override path exists for the customer — early access requires the early withdrawal flow (see below).

3. Auto-release on expiry

A scheduled job runs daily. For all lodgements where withdrawal_date = today and status = pending: the withdrawal posting is executed via MOD-001, the lodgement status is updated to withdrawn, and MOD-007 transitions the account back to active (or closed if the full balance was withdrawn). MOD-063 dispatches a "Your funds are now available" notification to the customer.

4. Early withdrawal

Customer requests early access by cancelling their notice. The module calculates the penalty:

penalty = notice_period_days × (annual_rate / 365) × amount

The penalty amount is displayed to the customer via the MOD-050 disclosure gate before any action is taken. The customer must explicitly acknowledge the penalty amount to proceed. On confirmation: the withdrawal and the penalty debit are posted as separate ledger entries via MOD-001. The lodgement record is updated to cancelled with the penalty amount recorded.

5. Liquidity reporting

MOD-130 provides MOD-032 (LCR/NSFR engine) with a daily snapshot of notice account balances bucketed by withdrawal date: within 30 days, 31–60 days, and 61–90 days. These buckets feed directly into the stable funding ratio calculation, ensuring notice deposits are not counted as at-call liquidity.

Requirements satisfied

FR-549 through FR-552.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy GATE Early withdrawal penalty is calculated and disclosed to the customer before any early withdrawal is processed — the penalty amount cannot be bypassed.
CLQ-002 — Liquidity Risk Management Policy CALC Notice account balances and their withdrawal dates are reported to the liquidity engine as non-callable until the notice period expires, contributing to the stable funding ratio calculation.
CON-001 — Customer Fairness & Conduct Policy AUTO Withdrawal is released automatically on the notice expiry date without requiring manual intervention — no customer is held beyond their contracted notice period.
CON-004 — Product Disclosure & Sales Practice Policy AUTO Customer receives confirmation at notice lodgement, a reminder 7 days before the withdrawal date, and a notification on the day the withdrawal becomes available.

MOD-133 — Trust account management

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Manages the opening, ongoing governance, and closure of trust accounts. A trust account is held by one or more trustees on behalf of beneficiaries under a trust deed. Trust accounts are common in NZ and AU for family trusts, testamentary trusts, charitable trusts, and commercial trusts. They require distinct account-opening workflows because beneficial ownership rules require the bank to identify and verify not only the trustee(s) but also any beneficial owner with a material interest — and to obtain and retain the governing trust deed.

Regulatory context

Beneficial ownership rules. FATF Recommendation 25 requires financial institutions to understand the beneficial ownership and control structure of legal arrangements, including trusts, before establishing a business relationship. Both the NZ and AU AML/CFT regimes implement this requirement directly.

NZ AML/CFT Act. Section 22 of the Anti-Money Laundering and Countering Financing of Terrorism Act 2009 requires reporting entities to conduct customer due diligence on the beneficial owners of trusts. Beneficial owners include trustees (as the legal owners), any settlor, any beneficiary who has received a distribution, and any person with effective control. Enhanced CDD is required where the trust structure presents higher risk — which applies to all trusts as a category under FATF guidance.

AU AML/CTF Act. The Anti-Money Laundering and Counter-Terrorism Financing Act 2006 and AUSTRAC rules require identification of beneficial owners of trusts. For discretionary trusts, the beneficial owner is the trustee and the settlor; for fixed trusts, beneficiaries with ≥ 25% interest must also be identified. The 2024 Tranche 2 reforms extended these obligations further.

The practical effect is that every trust account opening requires: (a) eIDV for all trustees, (b) eIDV for all beneficial owners with ≥ 25% interest, (c) enhanced CDD review, and (d) storage of the trust deed.

Trust types

Trust type Trustees Beneficial owners KYC requirements Notes
Family trust (discretionary) 1–3 typically Discretionary — no fixed beneficial interest All trustees + settlor; beneficiaries identified but not necessarily eIDV'd unless ≥ 25% interest received Most common trust type in NZ and AU
Testamentary trust Executor / appointed trustee Defined by will All trustees; named beneficiaries with ≥ 25% interest Established on death of testator
Charitable trust Trustees (board members) Public benefit — no individual beneficiaries All trustees (as governing persons) Registered charitable trusts may have reduced risk rating
Commercial trust (unit trust) Corporate trustee or individuals Unit holders with ≥ 25% interest All trustees + all unit holders ≥ 25%; full enhanced CDD Higher complexity; may trigger additional FATF obligations

Data model

-- core.trust_accounts
CREATE TABLE core.trust_accounts (
  trust_account_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id            UUID NOT NULL REFERENCES core.accounts(account_id),
  trust_name            TEXT NOT NULL,
  trust_type            TEXT NOT NULL CHECK (trust_type IN (
                          'family_discretionary', 'testamentary', 'charitable', 'commercial_unit'
                        )),
  trust_deed_document_id UUID NOT NULL,   -- MOD-073 document vault reference
  established_date      DATE,
  jurisdiction          TEXT NOT NULL CHECK (jurisdiction IN ('NZ', 'AU', 'NZ + AU')),
  enhanced_dd_completed BOOL NOT NULL DEFAULT false,
  next_review_date      DATE,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- core.trust_parties
CREATE TABLE core.trust_parties (
  party_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  trust_account_id      UUID NOT NULL REFERENCES core.trust_accounts(trust_account_id),
  customer_id           UUID NOT NULL REFERENCES core.customers(customer_id),
  party_role            TEXT NOT NULL CHECK (party_role IN (
                          'trustee', 'beneficial_owner', 'appointor', 'protector'
                        )),
  ownership_pct         NUMERIC(5,2),    -- null for discretionary trusts or non-ownership roles
  kyc_status            TEXT NOT NULL CHECK (kyc_status IN (
                          'pending', 'in_progress', 'verified', 'failed'
                        )) DEFAULT 'pending',
  eidv_completed        BOOL NOT NULL DEFAULT false,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX ON core.trust_parties (trust_account_id);
CREATE INDEX ON core.trust_parties (customer_id);

ownership_pct for beneficial owners in discretionary trusts is stored as null — there is no fixed beneficial interest. This is a valid state; the bank must still identify the class of beneficiaries but cannot assign percentages. The ≥ 25% threshold only applies where a fixed interest exists.

Key operations

Account opening with beneficial ownership mapping. An agent initiates a trust account opening request and records the trust name, type, jurisdiction, and establishment date. All trust parties are enrolled — trustees, beneficial owners (with ownership percentage where fixed), appointors, and protectors. For each trustee and each beneficial owner with ≥ 25% interest, eIDV is initiated via MOD-009. The trust deed is uploaded to MOD-073 and the returned trust_deed_document_id is written to the trust account record. Enhanced CDD is performed covering the trust structure, source of wealth, and purpose of the account. The account is held in a pending state until all required KYC is complete, enhanced_dd_completed is true, and trust_deed_document_id is populated. On completion, bank.core.trust_account_activated is emitted.

Trustee change workflow. When a trustee is added or removed, a change request is created. The outgoing trustee's removal is recorded and the incoming trustee's eIDV is initiated via MOD-009. A CDD review is triggered automatically (FR-595). The change does not take effect on the account until the incoming trustee's KYC is complete and the CDD review is resolved. A new or amended trust deed (deed of trustee retirement and appointment) must be uploaded to MOD-073 and a new version reference written to the trust account record.

Periodic CDD review. The next_review_date is set at account opening based on the account's AML risk rating. On the review date, a review task is created and assigned to the relevant compliance officer. The review covers all trust parties' current KYC status, any changes in beneficial ownership, the account's transaction profile, and any adverse media or screening alerts. On completion, next_review_date is advanced and enhanced_dd_completed is refreshed.

Account closure. Closure requires confirmation that all outstanding balances are settled and no open transactions are pending. The trust account record is updated with the closure date. The trust deed and all party records are retained in the document vault and audit log in accordance with the bank's data retention policy. The ledger account is closed via MOD-001.

Requirements

ID Requirement
FR-593 System shall prevent a trust account from activating until all trustees and all beneficial owners with an ownership interest ≥ 25% have individually completed eIDV via MOD-009, have been assigned a CDD risk tier, and the trust deed has been uploaded to MOD-073; partial KYC completion must not allow the account to open.
FR-594 System shall record all trust parties — trustees, beneficial owners, appointors, protectors — in core.trust_parties with their role, ownership percentage where applicable, and KYC status; the sum of beneficial owner percentages need not equal 100% (discretionary trusts have no fixed beneficial interest) but any party with ≥ 25% interest must pass enhanced due diligence.
FR-595 System shall trigger a trust account CDD review when any of the following occur: a trustee is added or removed, a beneficial owner change is notified, the account's AML risk rating is elevated by MOD-034, or the configured periodic review date is reached; the review must be completed within 30 days of the trigger event.
FR-596 System shall store the trust deed and any deed of variation in MOD-073 with a reference from the trust account record; the trust deed reference must be present before the account can activate; replacement deeds must be stored as a new version, retaining the previous version for audit purposes.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy GATE All trustees and beneficial owners with ≥ 25% interest must individually pass eIDV and CDD before the trust account activates — the account cannot open with partial KYC completion.
AML-004 — Politically Exposed Persons (PEP) Policy GATE Trust accounts are treated as inherently higher-risk for AML purposes; enhanced due diligence is required for all trust accounts at opening and on any trigger event before the account or change takes effect.
PRI-001 — Privacy Policy AUTO Each trustee's and beneficial owner's personal data is processed with individual consent; privacy rights (access and correction) are applied per person, not at the trust account level.
GOV-006 — Internal Audit Policy LOG All trust account opening events, trustee changes, beneficiary changes, and account governance events are logged as immutable records for regulatory and audit purposes.

MOD-134 — Community account management

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Community account management provides the account structure, signatory governance, and KYC enforcement required to hold and operate accounts on behalf of unincorporated and incorporated associations — sports clubs, community groups, religious organisations, residents' associations, and similar entities. It enforces multi-signatory authority rules at the transaction layer, manages annual committee turnover, and satisfies the AML beneficial ownership requirements that apply to association accounts.

Context

Community and club accounts are distinct from personal accounts and from standard business accounts. The account holder is the entity — the club or association — not any individual member. Authority over the account is vested collectively in committee officers, and that committee changes on an annual cycle, often at an AGM. The platform must handle:

  • Unincorporated associations — no separate legal personality; the officers are personally liable; the entity has no ACN/NZBN; the governing document is a constitution or set of rules
  • Incorporated societies — a registered legal person under the Incorporated Societies Act (NZ) or Associations Incorporation Act (AU states); holds an NZBN or ABN; the governing document is the society's constitution
  • Charitable entities — may be incorporated or unincorporated; registered with Charities Services (NZ) or the ACNC (AU); tax status is separate from account structure and is not recorded here
  • Body corporates — unit title bodies with legislated governance structures; less common as deposit account holders but present in the portfolio

The signing rule defines how transactions are authorised. Three modes are supported: any_one (any single signatory may authorise), any_two (two distinct signatories must both approve), and all (every active signatory must approve). The signing rule is set at account opening and recorded in the core.community_accounts table; it is enforced at the transaction authorisation layer, not solely in the UI.

Regulatory considerations

AML / beneficial ownership

The AML/CFT regime requires identification of beneficial owners — individuals who ultimately own or control 25% or more of the customer entity. For most community associations this threshold is not met by any individual: no person owns a share of the club. The relevant AML concept shifts from beneficial ownership to controlling persons — the individuals who exercise effective control over the entity through their committee roles.

At account opening, the platform must:

  1. Identify the entity type and obtain the governing document (constitution or rules)
  2. Identify all committee officers with authority to transact on the account (the authorised signatories)
  3. Complete individual eIDV (via MOD-009) for each of those signatories before the account activates
  4. Record the source of authority (the resolution or excerpt from the governing document confirming each person's role)

No beneficial owner threshold calculation is applied to the entity itself. Instead, the KYC obligation is satisfied by verifying all authorised signatories individually. This approach is consistent with the FATF guidance on non-profit organisations and the AML/CFT programme requirements under the NZ AML/CFT Act 2009 and the AU AML/CTF Act 2006.

Where the entity holds a charity registration number, NZBN, or ABN, those identifiers are recorded but are not a substitute for individual signatory eIDV.

Annual turnover and KYC refresh

Committee positions rotate annually. When a new committee member is added as an authorised signatory, they must complete eIDV before they can transact. Outgoing signatories must be deactivated (valid_until = today) at the point of the authority change, not at year end. The platform provides an annual signatory refresh workflow (see Key operations) that is initiated by an existing signatory with sufficient authority.

Data model

-- The community entity attached to an account
CREATE TABLE core.community_accounts (
    community_account_id  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    account_id            UUID NOT NULL REFERENCES core.accounts(account_id),
    entity_name           TEXT NOT NULL,
    entity_type           TEXT NOT NULL CHECK (entity_type IN (
                              'unincorporated_association',
                              'incorporated_society',
                              'charitable_trust',
                              'body_corporate'
                          )),
    constitution_document_id  UUID REFERENCES documents.vault(document_id),  -- MOD-073
    abn_acn_nzbn          TEXT,          -- nullable; unincorporated entities have none
    signing_rule          TEXT NOT NULL CHECK (signing_rule IN (
                              'any_one', 'any_two', 'all'
                          )),
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Authorised signatories on a community account
CREATE TABLE core.community_signatories (
    signatory_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    community_account_id  UUID NOT NULL REFERENCES core.community_accounts(community_account_id),
    customer_id           UUID NOT NULL REFERENCES core.customers(customer_id),
    role                  TEXT NOT NULL CHECK (role IN (
                              'president', 'treasurer', 'secretary', 'authorised_signatory'
                          )),
    kyc_status            TEXT NOT NULL CHECK (kyc_status IN (
                              'pending', 'verified', 'expired', 'failed'
                          )),
    valid_from            DATE NOT NULL,
    valid_until           DATE,          -- null = currently active
    created_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

constitution_document_id references the document stored via MOD-073. abn_acn_nzbn is nullable because unincorporated associations do not have a registered identifier. signing_rule is enforced at the transaction authorisation layer; any change to the signing rule after account opening requires a new authority resolution to be uploaded to MOD-073 and is logged to MOD-047.

Key operations

Account opening

  1. Collect entity name, entity type, governing document (uploaded to MOD-073), ABN/ACN/NZBN if applicable, signing rule, and the identity of all authorised signatories
  2. Create a core.community_accounts record with kyc_status = pending
  3. Initiate individual eIDV via MOD-009 for each signatory — each signatory receives their own eIDV flow
  4. Block account activation until all signatories have kyc_status = verified
  5. Once all signatories are verified, set the account to active and log the activation to MOD-047

Annual committee change workflow

  1. An existing active signatory (with authority to manage the account) initiates a committee refresh
  2. They upload the updated authority resolution to MOD-073
  3. For each outgoing signatory: set valid_until = today; the signatory loses transaction authority immediately upon save
  4. For each incoming signatory: create a new core.community_signatories record with kyc_status = pending; trigger eIDV via MOD-009
  5. The incoming signatory gains transaction authority only after eIDV is verified
  6. Log the complete refresh event (all additions and removals) to MOD-047 with acting staff member and timestamp

Signatory suspension

Where a signatory's eIDV expires or is downgraded, their kyc_status is set to expired. The account monitors the active signatory count against the signing rule. If the number of verified signatories falls below the minimum required by the signing rule, the account is placed in a restricted state (outgoing transactions blocked) and the account holder is notified via MOD-063. Inbound credits continue normally.

Account closure

Closure requires authorisation consistent with the signing rule. All signatories are marked valid_until = today at closure. The governing document and authority resolution remain in the document vault (MOD-073) for the retention period required by the AML/CFT programme.

Requirements

FR-597 — System shall prevent a community account from activating until all designated authorised signatories have individually completed eIDV via MOD-009 and the association's governing document (constitution or rules) has been uploaded to MOD-073; the number of required signatories and the signing rule must be set at account opening and enforced on all subsequent transactions.

FR-598 — System shall enforce the community account's signing rule on all outbound transactions: any_one allows any single signatory to authorise; any_two requires approval from two distinct signatories; all requires approval from every active signatory; the rule must be enforced at the transaction authorisation layer, not only in the UI.

FR-599 — System shall support an annual signatory refresh workflow: the account holder (existing signatory with sufficient authority) can add new signatories, deactivate outgoing signatories, and upload an updated authority resolution to MOD-073; outgoing signatories must be set to valid_until = today and must lose transaction authority immediately; the refresh event must be logged to MOD-047.

FR-600 — System shall detect when all active signatories on a community account have had their KYC status degraded below the required threshold (e.g. expired eIDV) and must place the account in a restricted state that blocks outgoing transactions until at least the minimum number of signatories for the account's signing rule have refreshed their KYC; the account holder must be notified via MOD-063.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy GATE All authorised signatories must pass eIDV before the account activates; the account does not open with partial signatory KYC.
CON-001 — Customer Fairness & Conduct Policy AUTO All authorised signatories receive account communications and statements simultaneously.
GOV-006 — Internal Audit Policy LOG All signatory additions, removals, and authority changes are logged immutably with the date and acting staff member.
PRI-001 — Privacy Policy AUTO Each signatory's personal data is processed with individual consent; their data is not shared with other signatories beyond the minimum necessary.

MOD-140 — Chart of accounts and GL configuration

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Provides a governed interface for defining, maintaining, and amending the institution's chart of accounts and GL account code configuration. Every posting in MOD-001 is validated against account codes defined here. The module enforces four-eyes approval for all changes, propagates activated accounts to downstream reporting modules automatically, and validates regulatory mappings before activation.

Context

Every bank operates a bespoke chart of accounts shaped by its management reporting preferences, regulatory obligations, and product portfolio. Without a governed chart of accounts interface, adding or amending GL account codes requires a developer deployment — introducing delay, risk of error, and an unaudited change path.

The platform ships with a default chart of accounts pre-mapped to RBNZ BS2A and APRA ARS 720 classifications. This default chart is sufficient to satisfy regulatory reporting requirements without customisation and cannot be deleted — system accounts are flagged is_system_account = true and are immutable to back-office staff. Banks extend the default chart by adding their own account codes for management reporting, cost centre allocation, or product-level tracking.

Regulatory mapping is validated before activation: any account code referenced in an RBNZ or APRA return must carry a valid mapping to the relevant regulatory line item taxonomy. Accounts mapped to a retired or renamed regulatory line item are flagged for review and cannot be activated until the mapping is corrected.

Account type hierarchy

The chart of accounts follows the standard five-type hierarchy used across banking:

Account type Sub-type examples (banking)
Asset Cash and cash equivalents, loans and advances, investment securities, deferred tax asset
Liability Customer deposits, wholesale funding, accrued interest payable, deferred income
Equity Share capital, retained earnings, other comprehensive income reserves
Income Interest income (lending), fee income, trading income, other operating income
Expense Interest expense (deposits, wholesale), credit impairment charge, staff costs, technology costs, regulatory levies

Sub-types are free-text strings that enable grouping within management reporting and are not validated against a fixed taxonomy. Account type is validated against the five permitted values at activation.

Regulatory mapping

Each GL account can carry one or more regulatory_mappings entries — JSON objects identifying the regulatory return, the line item code within that return, and the jurisdiction. Before an account is activated, every mapping is validated against the platform's regulatory line item taxonomy:

  • If the line item code exists in the taxonomy and is live, the mapping is valid.
  • If the line item code has been retired or renamed, the account is held in pending_review status and cannot be activated until the mapping is corrected.
  • If no mapping is present and the account type implies regulatory reporting relevance (income, expense, certain asset and liability sub-types), a warning is surfaced to the approver — the approval is not blocked, but the proposer must acknowledge the absence of mapping.

The platform ships with regulatory line item taxonomies for RBNZ BS2A, RBNZ BS6, APRA ARS 720.0, and APRA ARS 740.0. These are updated by platform deployments when regulators amend their return templates.

Data model

-- core.gl_accounts
CREATE TABLE core.gl_accounts (
  account_code          TEXT PRIMARY KEY,
  account_name          TEXT NOT NULL,
  account_type          TEXT NOT NULL CHECK (account_type IN ('asset','liability','equity','income','expense')),
  account_subtype       TEXT,
  currency              TEXT NOT NULL DEFAULT 'NZD',
  is_system_account     BOOL NOT NULL DEFAULT false,
  regulatory_mappings   JSONB,           -- [{return, line_item_code, jurisdiction}]
  status                TEXT NOT NULL DEFAULT 'active'
                        CHECK (status IN ('active','inactive','pending_review')),
  parent_account_code   TEXT REFERENCES core.gl_accounts(account_code),
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- core.gl_account_proposals
CREATE TABLE core.gl_account_proposals (
  proposal_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_code      TEXT NOT NULL,
  account_name      TEXT NOT NULL,
  account_type      TEXT NOT NULL,
  account_subtype   TEXT,
  currency          TEXT NOT NULL DEFAULT 'NZD',
  regulatory_mappings JSONB,
  parent_account_code TEXT,
  change_type       TEXT NOT NULL CHECK (change_type IN ('create','modify','deactivate')),
  change_reason     TEXT NOT NULL,
  proposed_by       UUID NOT NULL,
  proposed_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  status            TEXT NOT NULL DEFAULT 'pending'
                    CHECK (status IN ('pending','approved','rejected','live')),
  reviewed_by       UUID,               -- must differ from proposed_by
  reviewed_at       TIMESTAMPTZ,
  review_comment    TEXT,
  effective_date    DATE NOT NULL,
  applied_at        TIMESTAMPTZ,
  CONSTRAINT no_self_approval CHECK (proposed_by != reviewed_by)
);

System accounts (is_system_account = true) do not accept proposals from the back-office panel — any attempt to submit a proposal against a system account is rejected at the application layer with a structured error.

Maker/checker workflow

The chart of accounts change workflow follows the same maker/checker pattern as MOD-127 (product configuration panel):

1. Propose. A finance or product analyst submits a proposal: new account code, modification to an existing account's name, sub-type or regulatory mapping, or deactivation of an unused account. The system validates the regulatory mappings at submission time and flags any issues for resolution before the proposal is submitted for review.

2. Review. A second authorised staff member — who cannot be the proposer — reviews the proposal. They may approve, reject, or return for revision. Approval requires the reviewer to acknowledge any regulatory mapping warnings.

3. Activate. On the effective date, approved proposals are applied: the core.gl_accounts table is updated, the change is logged to MOD-047 with full before/after state, and the updated chart is propagated to MOD-080 and any registered downstream reporting modules within 60 seconds.

4. Deactivated accounts. Deactivated accounts (status = inactive) remain in the table and are queryable for historical reporting. They cannot be selected as the target of new postings in MOD-001. Reactivation requires a new proposal.

Key operations

Add account. Submit a proposal for a new account code with type, sub-type, optional parent, currency, and regulatory mappings. Regulatory mapping validation runs at proposal submission. Four-eyes approval required before activation.

Modify account. Submit a proposal to change the name, sub-type, or regulatory mappings of an existing non-system account. The current values are captured in the proposal as before state for the audit log.

Deactivate account. Submit a deactivation proposal. The system checks whether the account has any live posting rules in MOD-001 or live regulatory mappings in MOD-080 that would be broken by deactivation — if so, a warning is surfaced. Deactivation does not delete historical data.

Regulatory mapping update. Regulatory mapping changes follow the same proposal workflow. A mapping change on an account used in regulatory returns is treated as a REP-004 GATE event and requires the regulatory mapping validation to pass before approval is permitted.

Requirements

FR-621 — Posting validation: every posting in MOD-001 must validate both the debit and credit leg account codes against core.gl_accounts with status active; postings referencing unknown or inactive GL codes must be rejected with a structured error identifying the invalid code.

FR-622 — System account immutability: system accounts flagged is_system_account = true must not be modifiable via the back-office proposal workflow; all chart of accounts changes must enforce four-eyes approval with the proposed_by != reviewed_by constraint at the database level.

FR-623 — Downstream propagation: activated GL account changes must be propagated to MOD-080 (statutory reporting) and registered downstream reporting modules within 60 seconds of activation; deprecated accounts must remain queryable for historical reporting but must not be selectable for new postings.

FR-624 — Regulatory mapping validation: each GL account's regulatory_mappings must be validated against the known regulatory line item taxonomy before activation; accounts mapped to retired or renamed line items must be flagged for review; the platform must ship with a default chart pre-mapped to RBNZ BS2A and APRA ARS 720 classifications.

Policies satisfied:

Policy Mode Description
REP-004 — Financial Statements Policy GATE Changes to GL account codes used in regulatory reporting must be reviewed and approved before taking effect; any code used in an RBNZ or APRA return must have a valid regulatory mapping before it can be activated.
GOV-006 — Internal Audit Policy LOG All chart of accounts changes are logged immutably with the proposer, approver, timestamp, and before/after state, providing a complete audit trail for internal and regulatory review.
REP-002 — Prudential Reporting Policy AUTO GL account definitions are automatically published to MOD-080 (statutory reporting) and MOD-082 (management reporting) when activated, ensuring report templates reference only current, active account codes.
GOV-007 — Conflicts of Interest Policy GATE All chart of accounts changes require four-eyes (maker/checker) approval before taking effect; the proposer must not be the approver, enforced at the database constraint level.

MOD-143 — Open Bank Resolution pre-positioning

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Purpose

Pre-position the platform so that a deploying deposit taker can execute an Open Bank Resolution event in compliance with the RBNZ DTA OBR Pre-positioning Standard. OBR is an RBNZ resolution tool that allows a distressed deposit taker to be kept open overnight and re-opened the following business day with depositor accounts partially frozen and partially accessible. This module ensures the platform is technically capable of entering a resolution state, partitioning balances across all deposit accounts, and operating channels in a restricted mode — at any time and on short notice.

What it does

Balance fields

Every deposit account carries two new fields managed by this module in cooperation with MOD-001:

  • obr_frozen_amount — NUMERIC, default 0. Holds the portion of the account balance subject to the OBR haircut. Zero in normal operation; populated atomically on resolution-state activation.
  • obr_available_amount — NUMERIC, mirrors the full account balance in normal operation. On activation, reduced to balance × (1 - haircut_pct). This is the amount customers can access after resolution.

Both fields are stored on the MOD-001 account record and updated only through the OBR partition transaction.

Resolution state

A resolution_state flag on the platform instance governs module behaviour across three values:

  • normal — pre-positioning not yet completed (development and staging only).
  • pre_positioned — the default production state. The platform is ready to execute OBR at any time. Balance fields exist but contain their normal values. No customer-facing change.
  • activated — RBNZ has issued a haircut instruction and the module has partitioned all deposit account balances. Channel restrictions are in force.

Production deployments must run continuously at pre_positioned. Verification that resolution_state = pre_positioned is available to the RBNZ on demand via a signed status endpoint.

Partition calculation

On activation, the RBNZ supplies a haircut percentage via a signed instruction payload (rbnz_instruction_ref). The partition calculation runs as a single database transaction covering all deposit accounts:

obr_frozen_amount   = current_balance × haircut_pct
obr_available_amount = current_balance × (1 - haircut_pct)

For joint accounts, MOD-125 balance_share_pct is applied per holder before the haircut is calculated, ensuring each depositor's Statutory Deposit Value (SDV) obligation is met correctly. The full partition — across all accounts — is applied atomically. Partial completion is not permitted; the transaction rolls back if any account cannot be updated.

Channel behaviour under activation

When resolution_state = activated:

  • Digital channels (app and internet banking, governed by MOD-068 and MOD-069) switch to a restricted mode. Customers see their obr_available_amount as their accessible balance. Outbound payments and transfers that would draw on the frozen portion are blocked with an OBR notice presented to the customer.
  • Teller channel (MOD-129) enforces the same available/frozen split. Teller UI displays both amounts and the haircut percentage.
  • Inbound credits continue to be received and are not frozen.
  • Account information queries (balance, transaction history) remain fully available.

Audit and event table

All OBR activity is written to core.obr_partition_events:

Column Description
event_id UUID primary key
activated_at Timestamp of partition execution
haircut_pct Haircut percentage applied
rbnz_instruction_ref Reference from RBNZ instruction payload
accounts_partitioned Count of accounts updated
total_frozen_amount Sum of all obr_frozen_amount values post-partition
total_available_amount Sum of all obr_available_amount values post-partition
activated_by Operator identity or automated trigger reference
status pre_positioned / activated / resolved

Records in this table are immutable. No update or delete path exists. Every haircut instruction received, every partition execution, and every resolution exit is appended as a new row.

Resolution exit

When RBNZ issues a resolution end notice, the module unwinds the partition flags. obr_available_amount is restored to the full balance and obr_frozen_amount is set to zero for accounts where no write-off applies. Where the RBNZ confirms a permanent haircut, the frozen amount is posted as a credit loss entry in MOD-001 and the account balance is reduced accordingly. The status field in core.obr_partition_events is updated to resolved with a timestamp.

Customer notification

Activation of resolution state triggers customer notifications dispatched via MOD-063. Notification content is pre-approved as part of the deploying institution's OBR communication plan and stored as a template. Notifications are sent to all affected depositors within the activation transaction completion.

Compliance reason

The RBNZ DTA OBR Pre-positioning Standard, issued under the Deposit Takers Act 2023, requires every licensed deposit taker to maintain systems that are technically capable of executing OBR at any time without material system changes. The Standard requires the institution to demonstrate pre-positioning readiness to the RBNZ on request. A deploying institution using this platform as its core banking system cannot satisfy its OBR condition of registration without this module. The pre_positioned state must be active in production continuously, not only at examination time.

Commercial reason

Completing OBR pre-positioning removes a NZ licensing blocker. Institutions that cannot demonstrate OBR readiness will not receive or retain their deposit taker licence under the DTA 2023. Beyond the licensing requirement, demonstrating that OBR capability was built into the platform from first principles — rather than retrofitted under regulatory pressure — signals maturity to the RBNZ during the licence application process and reduces the risk of additional conditions being imposed. It also shortens the path to licence variation when adding new deposit product types, since the regulator has already examined and accepted the resolution architecture.

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy LOG OBR resolution-state activation and partition events are logged as regulatory records for RBNZ examination.
OPS-001 — Business Continuity Policy AUTO Resolution-state activation triggers immediate operational controls — channel mode switches and account partition flags applied atomically.

MOD-161 — Transfer pricing

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Owns the treasury schema in the bank_core Neon database (SD01) and provides the bank-core side of the transfer pricing (TP) system: the rate store, the cost-of-funds read service, and the instrument-to-tenor matching logic.

Transfer pricing is a management accounting technique that assigns an internal cost (loans) or benefit (deposits) to each facility based on the prevailing market rate for matching-tenor funding. Without it, all product lines appear equally profitable regardless of how they consume or provide the bank's liquidity — long-tenor mortgages look the same as overnight call accounts. This module grounds those cost allocations in bank-core.

What this module provides

TP rate store — treasury.tp_rates Holds the daily rate grid across nine tenor buckets (ON, 1M, 3M, 6M, 1Y, 2Y, 3Y, 5Y, 10Y) for NZ and AU jurisdictions. Written daily by MOD-086's write-back Lambda via the tp_writeback_user Postgres role. Each row carries the effective date, curve source version identifier, and the liquidity premium overlay applied by Treasury — enabling the seven-year version history required for product P&L audit and regulatory review.

TP read service GET /internal/v1/treasury/tp-rates/latest?jurisdiction=NZ|AU — serves the current rate grid to bank-core consumers (daily accrual in MOD-005, product pricing, ROTE inputs in MOD-106). No SD06 dependency on the read path; latency target sub-100 ms P99.

Cost-of-funds application The applicable TP rate for any bank-core instrument is derived by matching its repricing tenor to the nearest tenor bucket in treasury.tp_rates. This matching logic lives in bank-core. Loan facilities carry a TP cost equal to the rate for their matched tenor; deposit facilities receive a TP benefit. The result is the daily cost-of-funds figure fed into accrual and NIM attribution.

Relationship with MOD-086

MOD-086 (bank-risk-platform, SD06) performs the curve computation: reads market.swap_curve and market.ois_curve from Snowflake, applies the Treasury-configured liquidity premium overlay, and writes the resulting rate grid to both ftp.transfer_prices (Snowflake Dynamic Table) and treasury.tp_rates (this module's table) via the write-back Lambda. MOD-086 also produces ftp.nim_attribution in Snowflake by matching CDC-sourced balances against the rate grid for management accounts.

The split: MOD-086 owns the computation and the Snowflake analytics. MOD-161 owns the Postgres rate store, the bank-core read service, and the instrument-level cost-of-funds application.

Known degraded state on first deploy

The V001 migration GRANT to tp_writeback_user is wrapped in a conditional block that skips if the role does not yet exist in the target environment. MOD-086 write-backs continue recording status='FAILED' in ftp.writeback_runs until MOD-104 provisions the tp_writeback_user Postgres role and a V002 GRANT migration is applied. See bank-core/docs/design/treasury-bootstrap.md §7.

Policies satisfied:

Policy Mode Description
CLQ-002 — Liquidity Risk Management Policy CALC treasury.tp_rates stores the cost-of-funds rate for each repricing tenor bucket — bank-core consumers (accrual, pricing) read TP rates directly from Postgres, ensuring every interest rate risk measurement uses liquidity-adjusted cost allocations without an SD06 round-trip.
CLQ-003 — Capital Planning & Stress Testing Policy CALC treasury.tp_rates retains a full version history with effective date, curve source version, and liquidity premium basis points — providing the immutable Postgres audit trail required for capital stress scenario reconstruction under RBNZ and APRA review.

MOD-166 — Transaction category corrections

System: SD01 | Repo: bank-core | Build status: Deployed | Deployed: Yes

Captures customer and back-office corrections to transaction categorisation, satisfying FR-239. Owns the accounts.transaction_overrides table — an append-only Cat 1 immutable register of every category override applied to a posting.

A single Lambda (IAM_AUTH Function URL) accepts POST /internal/v1/transactions/{posting_id}/overrides. The handler validates the posting_id belongs to the claimed customer (via accounts.postings → accounts.account_party_relationships), records the correction, and returns the created row. No correction is ever mutated — a customer changing their mind writes a new row; the latest created_at wins for downstream retraining purposes.

The table feeds MOD-041's weekly retrain via the MOD-042 CDC pipeline. MOD-041 is bootstrap-resilient and operates without corrections until this module is deployed. Once deployed and the CDC path is wired, stg_customer_corrections in MOD-041's dbt project automatically picks up the live corrections data — no MOD-041-side code change required.

CDC inclusion is a separate bank-platform/MOD-042 action (file handoff to add accounts.transaction_overrides to Firehose routing + Glue + Snowflake External Table). Non-blocking for MOD-166's own build.

Policies satisfied:

(No policies assigned)


SD02 — Customer Identity & KYC Platform

Repo: bank-kyc | Business domain: BD01 | Tech owner: Identity & Compliance Engineering | Build status: Not started

End-to-end identity verification, KYC/CDD lifecycle management, PEP/sanctions screening at onboarding, and ongoing monitoring.

Modules

ID Name Status
MOD-009 eIDV & document verification Not started
MOD-010 CDD tier assignment engine Not started
MOD-011 KYC periodic review scheduler Not started
MOD-012 KYC audit trail store Not started
MOD-013 Real-time sanctions screener Not started
MOD-014 List change propagation Not started
MOD-015 False positive management Not started

For full module specifications and acceptance criteria, see module specifications.

Risk score mirror table

MOD-039 (customer risk score model, SD06) publishes bank.risk-platform/customer_risk_score_updated events whenever a customer's risk tier changes. bank-kyc maintains a local mirror table populated by MOD-010 (CDD tier assignment engine) from this event stream:

bank_kyc.party.risk_scores_mirror

Column Type Notes
party_id UUID PK; FK → party.parties.party_id
composite_risk_score FLOAT NOT NULL 0–100
risk_tier VARCHAR NOT NULL LOW | MEDIUM | HIGH | CRITICAL
score_version VARCHAR NOT NULL Model version from MOD-039
scored_at TIMESTAMPTZ NOT NULL Timestamp from the event
mirror_updated_at TIMESTAMPTZ NOT NULL DEFAULT now() When the mirror row was written

Idempotency key: (party_id, scored_at). MOD-010 upserts on receipt; no duplicate rows per scored moment. The mirror is read-only from KYC's perspective — SD06 owns the source of truth.

Critical constraints

  1. MOD-009 is a hard GATE — account state machine (MOD-007) will not activate any account without kyc_status = Verified.
  2. MOD-013 is a hard GATE — no payment can proceed for a confirmed sanctions match under any circumstances.
  3. All KYC decisions must be written to the immutable audit trail (MOD-012) before the decision takes effect.
  4. Sanctions list re-screening must run automatically within 1 hour of any list update.
  5. No Snowflake calls inline — customer risk score is read from the risk_scores_mirror Postgres write-back table (populated by MOD-010 from SD06 EventBridge events).

Modules in SD02


MOD-009 — eIDV & document verification

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

What it does

Calls government document verification services (DVS AU / DIA NZ), performs biometric liveness detection, and confirms identity via credit bureau. Produces a composite identity confidence score (0.0–1.0). This is the front gate of the KYC pipeline — no account can be activated without this module returning kyc_status = Verified.

Why it exists

Compliance: Satisfies AML-003 (KYC & Identity Verification Policy), derived from AML/CFT Act 2009 s15 (NZ) and AML/CTF Act 2006 Part 2 (AU). Customer identification is mandatory before any account is opened or product provided. The GATE satisfaction mode means this obligation is enforced structurally — it cannot be bypassed.

Commercial: Delivers BG-001 (Frictionless digital onboarding) and FR-001 (eIDV in real time without manual review). Removing manual identity review from the onboarding path is the primary conversion rate driver. Target: 90th percentile customer completes in under 5 minutes including this step (NFR-002).

Satisfaction mode: GATE

This module is a hard gate. The account state machine (MOD-007) checks kyc_status before any Pending → Active transition. If kyc_status is not Verified, the transition is refused. There is no agent override path, no exception process, and no bypass flag. A customer who cannot be verified is routed to EDD review.

Inputs

Input Source Notes
Full name, date of birth, address Onboarding form Stored in customers table
Document type and image Mobile app upload Passport / driver licence / national ID
Selfie / liveness capture Mobile app Anti-spoofing required
Jurisdiction Account application Determines which DVS service to call

Outputs

Output Destination Notes
kyc_status customers.kyc_status Verified / Failed / Pending_EDD
identity_confidence_score kyc_records.confidence_score 0.0–1.0
cdd_tier (triggers MOD-010) kyc_records.cdd_tier Standard / Simplified / Enhanced
provider_reference kyc_records.provider_ref For audit trail and dispute resolution
kyc.verified or kyc.failed event Kafka bank.kyc.events Triggers downstream processing

External dependencies

Service Provider Jurisdiction Purpose
Document verification DVS gateway (Home Affairs) AU Passport, driver licence, Medicare check
Document verification DIA API NZ Passport, NZTA driver licence check
Biometric liveness Onfido Both Selfie match, liveness, anti-spoof
Identity confirmation Equifax AU AU Credit bureau name/DOB confirmation
Identity confirmation Centrix NZ Credit bureau name/DOB confirmation

Data schema

-- kyc_records (Postgres — bank-kyc schema)
CREATE TABLE kyc_records (
  id                    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id           uuid NOT NULL REFERENCES customers(id),
  kyc_status            text NOT NULL CHECK (kyc_status IN ('Pending','Verified','Failed','Pending_EDD')),
  confidence_score      numeric(4,3) CHECK (confidence_score BETWEEN 0 AND 1),
  cdd_tier              text CHECK (cdd_tier IN ('Simplified','Standard','Enhanced')),
  provider              text,             -- 'onfido' | 'dva' | 'dia'
  provider_reference    text,
  jurisdiction          text NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  created_at            timestamptz NOT NULL DEFAULT now()
  -- append-only enforced by trigger: no UPDATE or DELETE
);

-- customers.kyc_status is a denormalised fast-read field
-- updated by trigger on kyc_records INSERT

Confidence score routing

Score Outcome CDD tier assigned
≥ 0.90 Verified — Standard CDD Standard
0.70–0.89 Verified — proceed, flag for periodic review Standard
0.50–0.69 Pending_EDD — route to EDD manual review Enhanced
< 0.50 Failed — customer cannot proceed N/A

Error handling

Failure Behaviour Customer impact
DVS / DIA API unavailable Retry 3× with backoff, then queue for retry in 15 min Customer notified of delay
Onfido timeout Single retry, then treat as confidence 0 for liveness component May route to EDD
Confidence < 0.50 Hard fail — audit trail entry written Customer contacts support
Suspected fraud signal from Onfido Hard fail, flag to compliance Account not opened

NFRs that apply

  • NFR-002: End-to-end onboarding ≤ 5 minutes p90 — this module must complete in ≤ 30 seconds in normal conditions
  • NFR-001: Onboarding completion rate ≥ 80% — failure rate here directly affects this metric

Policies satisfied:

Policy Mode Description
AML-003 — Know Your Customer (KYC) & Identity Verification Policy GATE Account cannot be activated without verified KYC — no bypass path exists
AML-002 — Customer Due Diligence (CDD) Policy AUTO CDD tier determined automatically from eIDV confidence score — not agent discretion
PRI-001 — Privacy Policy AUTO Identity data collected only for verification purpose — purpose limitation enforced at collection
CON-001 — Customer Fairness & Conduct Policy AUTO Same verification checks applied consistently to all customers — no discriminatory variation

MOD-010 — CDD tier assignment engine

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Assigns Standard, Simplified, or Enhanced CDD tier to every customer based on risk factors. Tier is re-evaluated on each trigger event.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy AUTO CDD tier set by rule engine — not agent discretion. EDD triggered automatically for PEPs and high-risk jurisdictions
AML-004 — Politically Exposed Persons (PEP) Policy ALERT PEP detection triggers EDD tier and senior management notification flag — no human decision required to escalate
GOV-002 — Risk Appetite Statement Policy GATE Customer risk score within RAF thresholds — auto-decline or auto-refer above appetite

MOD-011 — KYC periodic review scheduler

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Schedules and tracks periodic KYC reviews — Standard (3yr), EDD (1yr). Sends renewal requests, tracks responses, escalates on non-response.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy AUTO Periodic CDD review completed within required timeframe — no manual calendar management
AML-003 — Know Your Customer (KYC) & Identity Verification Policy ALERT Identity re-verification triggered automatically when documents expire
CON-001 — Customer Fairness & Conduct Policy AUTO Review cadence consistent across all customers of same tier — no discretionary skipping

MOD-012 — KYC audit trail store

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Immutable record of every KYC check, document upload, decision, and agent action. Stored with timestamp, operator ID, and check result.

Policies satisfied:

Policy Mode Description
AML-001 — AML/CFT Programme Policy LOG AML programme can be evidenced to regulator — every check and decision is logged
AML-002 — Customer Due Diligence (CDD) Policy LOG CDD decisions are auditable — regulator can reconstruct any customer's onboarding decision
GOV-006 — Internal Audit Policy LOG Internal audit can independently verify KYC decisions without relying on agent memory
PRI-005 — Privacy Impact Assessment Policy LOG PIA evidence trail maintained — data collected for KYC is documented and bounded

MOD-013 — Real-time sanctions screener

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Screens customers against OFAC, UN, MFAT, and DFAT lists at account creation and on each payment. Materialised in Postgres for sub-millisecond payment gate performance.

Policies satisfied:

Policy Mode Description
AML-007 — Sanctions Screening Policy GATE No payment can be made to or from a confirmed sanctions match — enforced as hard GATE, not advisory
PAY-001 — Payment Operations Policy GATE Payment processing blocked for sanctioned parties before funds move
AML-006 — Suspicious Activity Reporting Policy ALERT Confirmed sanctions hit creates automatic SAR/STR draft and escalation to compliance
GOV-002 — Risk Appetite Statement Policy AUTO Sanctions exposure maintained at zero — RAF threshold enforced by system not process

MOD-014 — List change propagation

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

When sanctions lists are updated, the engine re-screens all active customers automatically. Positive hits trigger immediate account restriction.

Policies satisfied:

Policy Mode Description
AML-007 — Sanctions Screening Policy AUTO Existing customers screened against new designations without manual trigger — no gap between list update and re-screening
AML-006 — Suspicious Activity Reporting Policy ALERT New designation matches auto-escalated to compliance — no reliance on agent to check the list

MOD-015 — False positive management

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Tracks adjudication decisions on potential sanctions matches. Confirmed false positives are recorded with operator ID and reasoning.

Policies satisfied:

Policy Mode Description
AML-007 — Sanctions Screening Policy LOG False positive decisions are auditable — reasoning recorded, not just the outcome
GOV-006 — Internal Audit Policy LOG Compliance team adjudication decisions logged for audit review and QA sampling

MOD-096 — Multi-entity party graph manager

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

What it does

MOD-096 is the multi-entity party graph manager. It extends the base customer party model to support a single authenticated user who operates across multiple legal and economic entities simultaneously — as themselves personally, as a sole trader, as a director of one or more companies, and as a property investor.

The problem it solves

A sole trader who also has a Ltd company and two rental properties is four distinct economic contexts under one login. Without a party graph: - All transactions appear in one undifferentiated stream - There is no way to view a clean P&L per entity - Classification signals for one context bleed into another - Xero/MYOB integration has no clean entity boundary to export to

MOD-096 creates the graph that separates these contexts while keeping them navigable under a single login.

Party graph structure

Each node in the graph is a party — a legal or economic entity with a type: - NATURAL_PERSON — the individual customer - SOLE_TRADER — the same individual acting in a business capacity (no separate legal entity in NZ/AU) - COMPANY — a Ltd company in which the customer is a director or shareholder - TRUST — a family trust or other trust with the customer as trustee or beneficiary - PROPERTY_CONTEXT — a non-legal operating context for a specific rental property (created by MOD-094)

Edges in the graph represent the relationship type: IS_DIRECTOR_OF, IS_TRUSTEE_OF, IS_BENEFICIAL_OWNER_OF, OPERATES_AS.

CDD requirement

Each entity node must have its own Customer Due Diligence profile. Adding a new entity to a customer's graph triggers the appropriate KYC/CDD check for that entity type via MOD-009 or MOD-010. A company cannot be added without verifying the UBO structure. A trust cannot be added without confirming the trustee arrangement.

Single-login multi-entity view

The customer app (SD08) uses the party graph from MOD-096 to: - Render a context switcher (e.g. "Ross — Personal | Ross Consulting | 12 Smith St") - Apply access controls and data scoping per context - Route classification, tax logic, and accounting mapper outputs to the correct entity context

Design phase

This module is in design. Build begins in Phase 2 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy GATE Each entity in the party graph must have its own CDD profile; MOD-096 cannot link a new entity to a party graph without triggering the appropriate KYC/CDD check for that entity type.
AML-004 — Politically Exposed Persons (PEP) Policy GATE Entity graph relationships with elevated risk indicators trigger enhanced due diligence via the existing EDD workflow before the relationship is confirmed.

MOD-153 — Customer acceptance engine

System: SD02 | Repo: bank-kyc | Build status: Deployed | Deployed: Yes

Purpose

The customer acceptance engine is the formal gate that converts KYC verification status, CDD risk tier, fraud score, sanctions result, and product-specific criteria into an explicit, auditable accept/decline/refer decision before any product account or facility can be opened. It is the platform's implementation of the FATF customer acceptance programme — the point at which the institution formally decides to enter a business relationship. Every decision is recorded with full input snapshot and rule trace, satisfying the documentary requirements for AML examination.

What it does

Acceptance rule evaluation

Evaluates a structured rule set for each product application, in sequence: (1) identity verification — must be verified via MOD-009; (2) sanctions screening — must be clear via MOD-015; (3) PEP status — PEP flag triggers EDD requirement; (4) onboarding fraud score — above threshold triggers REFER; (5) CDD tier achieved vs. CDD level required for the product (from MOD-127); (6) customer risk score from MOD-039 for higher-risk products; (7) jurisdiction and product eligibility; (8) product suitability for retail credit products. Rule thresholds are deployment parameters — the rule structure is fixed.

Decision outcomes

  • ACCEPT — all rules passed; product onboarding proceeds
  • DECLINE — hard-fail rule triggered (sanctions hit, failed identity, jurisdiction ineligible); customer notified with reason code
  • REFER — soft-fail rule (high risk score, PEP requiring EDD, fraud score above threshold); case created in MOD-151 (Risk Case Console) for compliance officer review; product onboarding held
  • HOLD_FOR_EDD — PEP or high-risk customer where EDD not yet complete; gated on EDD completion event from MOD-010; no manual action required unless EDD SLA expires

Formal risk rating record

The acceptance decision is written to kyc.acceptance_decisions: customer_id, product_id, decision, decision_at, methodology_version, inputs (JSONB snapshot of all rule inputs), applied_rules, triggered_rules, reason_codes, and decision_officer (null for automated, officer_id for compliance overrides). This record satisfies AML-012's requirement for a documented, auditable customer risk rating with methodology reference.

Adverse action notices

For DECLINE outcomes on credit products, an adverse action notice is generated citing the specific reason code, satisfying NZ CCCFA and AU NCCP obligations on credit decline communication. Non-credit product declines generate a standard notification via MOD-063.

Re-evaluation triggers

When a customer's CDD tier changes, PEP status is updated, or a sanctions hit is resolved, active product relationships are automatically re-evaluated. If a re-evaluation produces a DECLINE for an existing product, a compliance officer case is created in MOD-151 rather than an automatic account closure.

Compliance reason

AML-011 requires a customer acceptance programme that determines who the institution will and will not accept. Without a platform-enforced gate, this programme exists only as a policy — individual operators may admit customers who should have been declined. AML-012 requires a formal risk rating with documented methodology; the acceptance record is that rating. FATF Recommendations 10 and 17 require customer acceptance decisions to be documented and auditable for correspondent and high-risk customer relationships.

Commercial reason

A formally documented acceptance decision with a complete input snapshot is a valuable risk management and legal asset if the relationship later becomes problematic. The absence of a documented acceptance decision — especially for a high-risk customer — is a significant liability in a regulatory examination or a legal dispute.

Policies satisfied:

Policy Mode Description
AML-011 — Customer Acceptance Policy GATE No product account or facility can be activated until the acceptance engine has produced a formal ACCEPT decision — the gate is enforced at the service layer with no bypass path.
AML-002 — Customer Due Diligence (CDD) Policy GATE Acceptance enforces the CDD tier required for each product type — a customer who has not completed the required CDD level cannot be accepted regardless of KYC pass status.
AML-012 — Customer Risk Rating Policy CALC The acceptance decision record IS the formal customer risk rating — it carries the input snapshot, methodology version, decision outcome, and tier assignment required for AML examination.
AML-004 — Politically Exposed Persons (PEP) Policy GATE PEP status is an explicit rule input; a PEP customer cannot be accepted without EDD completion on record — the gate enforces this with no override path below the compliance officer role.
CON-006 — Product suitability and governance GATE Product suitability is evaluated as part of acceptance for retail credit products — a customer whose profile falls outside the product suitability criteria is referred, not silently passed.

SD03 — AML Transaction Monitoring Platform

Repo: bank-aml | Business domain: BD07 | Tech owner: Financial Crime Engineering | Build status: Not started

Continuous monitoring of all transactions for AML typologies, unusual patterns, and reportable activity. Produces alerts, manages cases, and submits regulatory reports automatically.

Modules

ID Name Status
MOD-016 Rule-based typology engine Not started
MOD-017 ML behavioural scoring model Not started
MOD-018 Alert case management system Not started
MOD-019 Regulatory report submission Not started

Architecture

All monitoring runs as Kafka consumers against bank.transactions. Rule engine (MOD-016) and ML scorer (MOD-017) both subscribe to the same event stream independently. Results written to Postgres for case management (MOD-018). See ADR-003 and ADR-010.

Risk score mirror table

MOD-039 (customer risk score model, SD06) publishes bank.risk-platform/customer_risk_score_updated events whenever a customer's risk tier changes. bank-aml maintains a local mirror table populated by MOD-016/017 from this event stream:

bank_aml.aml.risk_scores_mirror

Column Type Notes
party_id UUID PK; cross-domain ref to SD02 party.parties.party_id — no FK constraint (cross-database; application-enforced)
composite_risk_score FLOAT NOT NULL 0–100
risk_tier VARCHAR NOT NULL LOW | MEDIUM | HIGH | CRITICAL
score_version VARCHAR NOT NULL Model version from MOD-039
scored_at TIMESTAMPTZ NOT NULL Timestamp from the event
mirror_updated_at TIMESTAMPTZ NOT NULL DEFAULT now() When the mirror row was written

Idempotency key: (party_id, scored_at). MOD-016/017 upserts on receipt. The mirror is read-only from AML's perspective — SD06 owns the source of truth. AML typology rules (MOD-016) and ML scoring (MOD-017) read from this mirror rather than making cross-bus Snowflake calls inline.

Modules in SD03


MOD-016 — Rule-based typology engine

System: SD03 | Repo: bank-aml | Build status: Deployed | Deployed: Yes

Configurable rule set covering FATF typologies — structuring, rapid movement, round-tripping, unusual cash patterns. Rules evaluated on each Kafka transaction event in near-real-time.

Policies satisfied:

Policy Mode Description
AML-005 — Transaction Monitoring Policy AUTO All transactions monitored against typology rules — no sampling, no gaps
AML-001 — AML/CFT Programme Policy LOG AML programme includes documented, tested monitoring rules — regulator can inspect rule logic
AML-008 — Cross-Border Transfer Reporting Policy AUTO Cross-border transfers flagged automatically for IFTI/CMIR threshold check

MOD-017 — ML behavioural scoring model

System: SD03 | Repo: bank-aml | Build status: Deployed | Deployed: Yes

Snowflake Cortex model scores each customer's transaction against their own historical baseline and peer cohort. Anomalies generate risk score delta; high deltas queued for analyst review.

Policies satisfied:

Policy Mode Description
AML-005 — Transaction Monitoring Policy AUTO Behavioural anomalies detected without requiring a specific rule — model adapts to new patterns
DT-005 — Model Risk Management Policy LOG Model version controlled, validated, and logged — champion/challenger governance applied
AML-001 — AML/CFT Programme Policy LOG ML model forms part of documented AML programme — supervisors can inspect model and outputs

MOD-018 — Alert case management system

System: SD03 | Repo: bank-aml | Build status: Deployed | Deployed: Yes

Alerts routed to analyst queue with full customer context. Case decisions (dismiss/escalate/SAR) logged immutably with analyst ID and reasoning.

Policies satisfied:

Policy Mode Description
AML-005 — Transaction Monitoring Policy LOG Every alert is actioned and its disposition recorded — no alerts silently discarded
AML-006 — Suspicious Activity Reporting Policy LOG SAR decision trail — from alert to submission — fully auditable
GOV-006 — Internal Audit Policy LOG Compliance function performance measurable — alert volumes, aging, disposition rates reportable

MOD-019 — Regulatory report submission module

System: SD03 | Repo: bank-aml | Build status: Deployed | Deployed: Yes

Automatically identifies IFTI (AU) and CMIR (NZ) reportable transactions, formats to required schema, submits to AUSTRAC and RBNZ on schedule.

Policies satisfied:

Policy Mode Description
AML-008 — Cross-Border Transfer Reporting Policy AUTO IFTI and CMIR reports submitted automatically — no manual data extraction or formatting
REP-003 — AML Compliance Reporting Policy LOG Regulatory submission records maintained with submission timestamp and acknowledgement
AML-001 — AML/CFT Programme Policy AUTO Reporting obligations met without reliance on individual staff remembering to submit

SD04 — Payments Processing Platform

Repo: bank-payments | Business domain: BD06 | Tech owner: Payments Engineering | Build status: Not started

Real-time and batch payment processing across all rails — domestic NZ/AU, NPP, cross-border wallet, and card. Includes fraud detection, settlement, and scheme compliance.

Modules

ID Name Status ADR
MOD-020 Pre-payment validation suite Not started ADR-001
MOD-021 Payment limit & velocity controller Not started
MOD-022 Payment audit trail Not started
MOD-023 Transaction fraud scorer Not started ADR-010
MOD-024 Device & session intelligence Not started ADR-010
MOD-025 FX rate lock & conversion Not started ADR-015
MOD-026 IFTI / CMIR reporting trigger Not started ADR-015

For full module specifications and acceptance criteria, see module specifications.

Critical constraints

  1. MOD-020 is a hard GATE — no payment may proceed unless validation passes.
  2. MOD-013 (SD02) sanctions check must clear before any outbound cross-border payment.
  3. Balance authorisation must read from Postgres (SD01), never from Snowflake.
  4. FX conversion legs must be atomic — both sides post in a single transaction.

Modules in SD04


MOD-020 — Pre-payment validation suite

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Runs mandatory sequence before any payment is authorised: balance sufficiency, daily limit, sanctions screen, fraud score, account status. Any failure blocks. See ADR-001.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE No payment leaves the bank without passing all pre-flight checks — enforced as sequential gate
AML-007 — Sanctions Screening Policy GATE Sanctions screen is one of the mandatory pre-payment gates — cannot be bypassed
PAY-005 — Payment Fraud Prevention Policy GATE Fraud score gate applied before every payment — high-risk payments blocked or challenged
CLQ-002 — Liquidity Risk Management Policy CALC Payment volume and size contributes to intraday liquidity monitoring automatically

MOD-021 — Payment limit & velocity controller

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Enforces per-customer payment limits and velocity rules in real-time before any payment is executed. Supports six limit types: per-transaction, daily, weekly, monthly, 30-day rolling, and approval-threshold (FR-125 through FR-128). Limits are configured per-customer, per-payment-type, and per-channel with full audit history (FR-126). When a payment would breach a limit, the module returns a FAIL decision immediately with the breaching limit type and attempted/allowed values; it also emits bank.payments.limit_breach_detected for AML-005 structuring signal delivery. When a payment exceeds the approval threshold, it emits bank.payments.approval_required to trigger MOD-062's multi-step approval workflow. All limit check decisions are idempotent via a shared payments.idempotency_keys table. Limit checks are optimised for NFR-025 (p99 ≤ 20ms against active limits); alarms are provisioned for NFR-020 operational visibility.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy GATE Velocity limits prevent account takeover fraud patterns — enforced automatically
AML-005 — Transaction Monitoring Policy ALERT Structuring detection assisted by velocity rules — rapid small payments flagged
CON-005 — Fee & Pricing Transparency Policy AUTO Customer-set limits honoured immediately — no delay between setting and enforcement

MOD-022 — Payment audit trail

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Every payment instruction, validation result, routing decision, and ledger posting recorded with microsecond timestamp. Immutable.

Policies satisfied:

Policy Mode Description
PAY-002 — Settlement Risk Policy LOG Settlement disputes resolved using immutable payment audit trail
PAY-003 — Card Scheme Compliance Policy LOG Scheme compliance evidence — every card transaction has full processing record
REP-005 — Data Quality & Assurance Policy LOG Payment data lineage from instruction to GL posting fully traceable

MOD-023 — Transaction fraud scorer

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

XGBoost model producing fraud probability on each transaction in <200ms. ≥0.85 auto-decline. 0.60–0.85 step-up auth. <0.60 pass. See ADR-010.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy AUTO Fraud model runs on every transaction — not sampled. Score and decision logged.
CON-001 — Customer Fairness & Conduct Policy AUTO Customers protected from fraud automatically — not reliant on reporting it after the fact
DT-005 — Model Risk Management Policy LOG Fraud model versioned, validated, and performance-monitored in Snowflake

MOD-024 — Device & session intelligence

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Tracks device fingerprint, IP, location, and session behaviour. Flags new device, impossible travel, and session anomalies.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE Unauthorised device access detected and challenged without human SOC involvement
PAY-005 — Payment Fraud Prevention Policy ALERT Account takeover signals detected at device level before payment is attempted
AML-005 — Transaction Monitoring Policy LOG Device anomalies logged as AML monitoring signals — feeds behavioural model

MOD-025 — FX rate lock & conversion

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Locks a customer rate for 30–60 seconds. On confirmation, posts both legs atomically through the FX nostro. See ADR-015.

Policies satisfied:

Policy Mode Description
PAY-004 — Cross-Border Payments & FX Policy LOG FX rate applied to each conversion is locked and recorded — no post-hoc rate adjustment possible
CON-005 — Fee & Pricing Transparency Policy GATE Spread disclosed to customer before confirmation — system enforces pre-disclosure not post-disclosure
CLQ-004 — Interest Rate Risk in the Banking Book (IRRBB) Policy CALC FX position updated on each conversion — IRRBB and FX risk exposure current at all times

MOD-026 — IFTI / CMIR reporting trigger

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Every cross-border transfer evaluated against AUD 1,000 (AU) and NZD equivalent (NZ) thresholds. Qualifying transfers automatically tagged and batched for submission.

Policies satisfied:

Policy Mode Description
AML-008 — Cross-Border Transfer Reporting Policy AUTO No reportable transfer can be missed — threshold check applied to every cross-border event
REP-003 — AML Compliance Reporting Policy AUTO IFTI/CMIR batch prepared and submitted automatically — no manual extraction

MOD-061 — Open banking API platform

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

The open banking API platform is the Data Holder side of the platform's open banking capability. It exposes customer-consented account and transaction data to accredited third-party providers (TPPs) through a standards-compliant API layer, and manages the developer portal and TPP onboarding. Unlike a CDR-only implementation, the platform uses a jurisdiction profile model: the same gateway code serves every supported open banking regime. Enabling a new jurisdiction's open banking is a profile configuration, not a code change or new module deployment.

What it does

Jurisdiction profiles

The gateway ships with five profiles, each encapsulating the full specification of one open banking regime:

  • au_cdr — Consumer Data Standards v1.x (AU CDR)
  • nz_payments_nz — Payments NZ API Centre v2.x
  • uk_open_banking — OBIE/FAPI 1.0 Advanced
  • eu_psd2 — Berlin Group NextGenPSD2 v1.3
  • generic_fapi2 — FAPI 2.0 baseline for any jurisdiction not covered by the profiles above

Each profile defines:

  • Security profile and token binding requirements — the OAuth 2.0 / FAPI variant, PAR requirements, DPoP or MTLS binding
  • Consent schema — the fields required in a consent object for that jurisdiction, including mandatory and optional attributes
  • Scope and permission mapping — translation from the jurisdiction's permission model (e.g., CDR data clusters, OBIE permissions, Berlin Group consent categories) to internal data cluster identifiers
  • API endpoint structure and versioning convention — resource paths, versioning headers, and pagination format
  • Accreditation registry source — where TPP credentials are verified: AU CDR Register, NZ directory, UK Open Banking Directory, or equivalent
  • Rate limits and pagination conventions — per-jurisdiction defaults that can be overridden per TPP

Profile activation is controlled by the openbanking.profiles.enabled configuration list — for example, [au_cdr, nz_payments_nz]. Profiles not in the list are not exposed. Multiple profiles can be active simultaneously on the same gateway instance.

Profile isolation is strict: each active profile has its own base path (/cdr/v1/, /nz/v2/, /uk/v3/, etc.), its own accreditation check against the relevant registry, and its own consent scope validation. A token issued under one profile cannot be used against another profile's endpoints.

Adding a future jurisdiction is a platform update — a YAML profile definition plus an endpoint adapter. No schema migration, no new module, no code fork.

Data Holder API layer

The gateway exposes standardised resource endpoints across all active profiles: accounts list, account detail, transactions, balances, direct debits, payees, and customer profile. All responses are normalised to the platform's internal data model before profile-specific serialisation — the same account record renders as CDR JSON, Berlin Group JSON, or UK OBIE JSON depending on the active profile, but the upstream data source is identical.

Pagination, filtering, and date-range parameters are handled consistently across profiles and translated to the profile-specific wire format. Response schemas are validated before dispatch — malformed responses are rejected at the gateway and never returned to the TPP.

FAPI 2.0 — Demonstrating Proof of Possession (DPoP), Pushed Authorisation Requests (PAR), and Rich Authorization Requests (RAR) — is the common security baseline across all profiles. Profiles that require a stricter subset (e.g., MTLS in addition to DPoP) layer that requirement on top of the baseline.

JWT validation is delegated to MOD-044 (RBAC). All inbound tokens carry an ob_profile claim identifying the issuing profile, preventing cross-profile token reuse.

Every resource request is checked against the MOD-049 consent store before data is returned. The specific data clusters or permission codes requested must be present in an active, non-expired consent record for that TPP and customer combination. Revoked consents take effect within 60 seconds — cached consent checks have a maximum TTL of 60 seconds.

Developer portal and TPP onboarding

Each active profile has a self-service developer portal for TPP sandbox access, credential management, and API documentation. Documentation is generated from the profile's OpenAPI specification and is always in sync with the deployed endpoint version.

Production onboarding requires accreditation verification against the relevant jurisdiction registry before production credentials are issued. The onboarding flow is profile-specific: AU CDR uses the CDR Register software product record; NZ uses the Payments NZ directory; UK uses the Open Banking Directory. The generic_fapi2 profile requires manual accreditation review.

Audit and monitoring

Every API call is logged with: TPP identity, customer identity, profile, endpoint, data clusters returned, consent ID, response time, and HTTP status. Rate limit enforcement applies per TPP and per profile. Alerts fire on accreditation expiry, unusual data volume relative to consent scope, and consent anomalies (e.g., requests against a consent that was revoked within the TTL window).

Compliance reason

Open banking is a regulatory obligation in AU (CDR), a Payments NZ initiative in NZ, a mandate in UK (CMA9), and a mandate in EU (PSD2). The profile model means the platform satisfies all of these obligations from a single codebase. A deploying institution expanding from NZ to AU, or from AU to UK, activates the relevant profile without a platform rebuild. The FAPI 2.0 baseline security profile meets or exceeds the security requirements of every supported jurisdiction's standard, so the platform does not need per-jurisdiction security architecture decisions.

Commercial reason

TPP ecosystem access is a competitive differentiator — banks offering open banking APIs attract fintech partnerships and account migration flows. The profile model means the vendor's engineering investment in the gateway compounds across jurisdictions: a UK bank deploying this platform gets the same tested, hardened gateway as an AU bank. Time-to-market for open banking compliance in a new country drops from a multi-month build to a profile configuration and accreditation registration.

Policies satisfied:

Policy Mode Description
PAY-010 — Open Banking & API access GATE Enforces customer consent scope at the API layer — all third-party data requests blocked without valid, active consent.

MOD-067 — Trade finance operations

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Trade finance operations provides the operational layer for conditional payment instruments — letters of credit, bank guarantees, and standby LCs. These instruments underpin cross-border trade by providing payment assurance to beneficiaries while protecting buyers from paying before goods or services are delivered. The module manages the instrument lifecycle from issuance to final settlement or expiry.

The core workflow is document-driven: applicants submit trade documents (invoices, bills of lading, certificates of origin) through the app; ops staff review each document against the instrument's stated conditions using a structured checklist. Compliant presentations trigger the payment obligation via MOD-020 (pre-payment validation) and MOD-022 (payment audit trail). Discrepancies are flagged as exceptions for client resolution.

Required to support PRD-015 (Bank Guarantee / Letter of Credit). Designed to comply with ICC UCP 600 and ISP98 rules governing documentary credit operations, with all document presentations and compliance determinations retained in the immutable audit trail.

Policies satisfied:

Policy Mode Description
PAY-002 — Settlement Risk Policy GATE Validates all conditions before executing the payment obligation on an LC or guarantee — settlement is blocked until conditions are confirmed.
AML-008 — Cross-Border Transfer Reporting Policy LOG Logs all cross-border trade finance transactions and counterparty details for AML screening and reporting.

MOD-081 — Payment reconciliation engine

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

The payment reconciliation engine matches every payment instruction processed by the bank against the corresponding settlement confirmation from the interbank settlement system, and raises exceptions for unmatched or short-settled items.

Purpose

Payments are instructed in Postgres. Settlement is confirmed externally — by RBNZ ESAS (NZ), NPP/BECS (AU), or correspondent bank SWIFT confirmations for cross-border. A gap between instructed and settled creates a financial exposure: the bank may have posted a debit to a customer account without receiving funds in settlement, or may have received funds without posting a corresponding credit. Reconciliation closes this gap on every business day.

What it does

  • Ingest settlement files — receives intraday and end-of-day settlement files from ESAS, NPP, BECS, and SWIFT in structured format; files are parsed and stored in PAYMENTS.raw_settlements in Snowflake
  • Match to payment records — every settlement item is matched to the corresponding payments record in Postgres by payment reference, amount, value date, and counterparty account
  • Exception handling — unmatched items (payment instructed, no settlement), short-settled items (settled for less than instructed), and late-settled items (settled outside the value date window) are raised as exceptions in the reconciliation_exceptions table and routed to the payments operations work queue (MOD-064)
  • Intraday position — provides a live intraday net settlement position for the treasury (used by MOD-082 Nostro management)
  • Close-of-day sign-off — produces a daily reconciliation sign-off report confirming all items are matched or exception-managed; this is the formal daily settlement close

Reconciliation SLA

All exceptions must be investigated and resolved within the same business day. Open exceptions at 17:00 local time are escalated to the Head of Payments Operations and flagged in the MOD-036 prudential data feed.

Policies satisfied:

Policy Mode Description
PAY-002 — Settlement Risk Policy AUTO Every payment instruction is reconciled against settlement confirmation — unmatched items are escalated automatically before close of business.
REP-005 — Data Quality & Assurance Policy LOG Reconciliation outcomes are retained with full data lineage — payment, settlement file, and matched/unmatched status are traceable for audit and dispute resolution.

MOD-082 — Nostro & FX treasury management

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

The nostro and FX treasury management module maintains the bank's correspondent banking positions and manages intraday funding flows across all currency corridors.

Purpose

Cross-border and FX payments require the bank to hold pre-funded balances ("nostro accounts") at correspondent banks in each settlement currency. If a nostro account is underfunded, outward payments in that currency cannot settle. If it is overfunded, the bank holds excess liquidity earning below-market returns. This module manages both risks.

Nostro accounts

The bank holds nostro accounts at correspondent banks for each active currency corridor. Each account has a configured minimum balance (the funding floor) and a target balance. The module monitors the intraday balance of each nostro account in real time via the correspondent bank's balance reporting API or MT940/camt.052 message feeds.

What it does

  • Real-time balance monitoring — receives intraday balance messages from each correspondent and maintains a live nostro position in Postgres
  • Automatic funding triggers — when a nostro balance falls within the funding alert threshold (configurable per currency), the module raises a funding instruction to the treasury team via the operations work queue (MOD-064) and optionally executes an automated funding transfer if the treasury has pre-authorised auto-funding for that corridor
  • Payment gating — when a nostro balance is below the minimum required to fund a queued outward payment, the payment is held in MOD-020 (Pre-payment validation) until the nostro is funded or the treasury manually releases the hold
  • Position dashboard — provides the treasury team with a real-time view of all nostro positions, intraday settlement flows, and projected close-of-day positions across all corridors
  • Integration with reconciliation — feeds intraday nostro movements to MOD-081 (Payment reconciliation engine) for matching against outward payment settlements

Relationship to customer FX

Customer-facing FX conversion (rate lock, spread, conversion execution) is handled by MOD-025 (FX rate lock & conversion). This module operates at the bank's treasury level — it is invisible to customers but is the funding mechanism that makes customer FX payments possible.

Policies satisfied:

Policy Mode Description
CLQ-002 — Liquidity Risk Management Policy CALC Nostro balances at correspondent banks are included in the liquidity position calculation — LCR and NSFR capture all accessible liquidity pools.
PAY-002 — Settlement Risk Policy GATE Outward cross-border payments are blocked if the correspondent nostro account balance is insufficient to fund settlement — overdraft of nostro positions is not permitted.
PAY-008 — Payment Routing, Sponsor & Card-Scheme Abstraction Policy AUTO Nostro account positions are reconciled against correspondent bank statements on every settlement cycle — discrepancies are flagged automatically for treasury review.

MOD-084 — Open banking data access — data recipient

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

The open banking data recipient module enables the platform to act as a Data Recipient — obtaining customer-consented access to data held at another institution — across any supported open banking jurisdiction. This allows the platform to retrieve account, transaction, and identity data from a customer's existing bank to support account migration, affordability assessment, and account pre-population. Unlike a CDR-only data recipient, this module uses the same jurisdiction profile model as MOD-061 and MOD-049, connecting to external institutions over whichever open banking standard they expose.

What it does

Supported profiles and connection model

For each active jurisdiction profile, the module connects as an accredited Data Recipient:

  • AU CDR: registered as a Data Recipient on the CDR Register; connects to AU banks via Consumer Data Standards API
  • NZ (Payments NZ): registered with Payments NZ directory; connects to NZ banks via API Centre specification
  • UK Open Banking: registered with the UK Open Banking Directory (OBIE); connects via FAPI 1.0 Advanced / OBIE Read/Write API
  • EU PSD2: registered as an AISP/PISP with relevant NCA; connects via Berlin Group NextGenPSD2 API
  • Generic FAPI 2.0: connects to any institution exposing a FAPI 2.0 compliant API, using dynamic client registration

The connection layer is abstracted: each profile has a connector that handles that jurisdiction's token exchange, endpoint discovery, and response normalisation. The consuming workflow receives a normalised data structure regardless of source profile.

Use cases

Three consuming workflows use the data recipient capability:

  1. Account migration pre-fill: customer consents to retrieve their account list, direct debits, and payee list from their current bank. MOD-062 orchestrates the migration; this module fetches the data.
  2. Affordability assessment: customer consents to share transaction history. MOD-027 (affordability) receives normalised transaction data for income/expense analysis without screen-scraping.
  3. Account opening pre-fill: customer consents to share name, address, and contact details from their current bank to pre-populate the KYC form, reducing friction at onboarding.

Before any outbound request, MOD-049 consent is checked: (external_institution_id, customer_id, requested_scopes) must be valid and active.

Retrieved data is stored in a purpose-scoped temporary store with a TTL tied to the consuming workflow — data is deleted when the workflow completes or is abandoned, whichever is sooner.

Data is never retained beyond the declared purpose. Retention policy is enforced at the data layer, not application logic — TTL expiry triggers hard deletion.

All retrieval events are logged: external institution, profile, scopes requested, data received (volume only, not content), consuming workflow, and deletion timestamp.

Connection directory and discovery

Each profile maintains an institution directory (fetched from the relevant accreditation registry on a daily schedule) listing all Data Holders the platform can connect to, their endpoint discovery documents, and their accreditation status.

Customers see a searchable list of connectable institutions within the consent initiation flow.

Compliance reason

As a Data Recipient, the platform is subject to the data recipient obligations of each active jurisdiction — AU CDR Rules, NZ API Centre terms, OBIE Data Recipient terms, GDPR (EU). Normalising to a single retrieval module means the data handling obligations (purpose limitation, deletion, consent validation) are implemented once and applied consistently across all jurisdictions. A jurisdiction-specific implementation would duplicate these obligations with different code paths, increasing the risk of inconsistent behaviour.

Commercial reason

Data recipient capability is the unlock for frictionless account migration — the strongest customer acquisition tool for a challenger bank. Connecting to a customer's existing bank over open banking and pre-filling their direct debits and payees reduces the migration effort from hours to minutes. Expanding this to affordability pre-fill creates a faster, more accurate credit decision than self-declared income.

Policies satisfied:

Policy Mode Description
PAY-010 — Open Banking & API access GATE Customer consent for open banking data retrieval is validated against the MOD-049 consent store before any request is made to the external Data Holder — no retrieval without explicit, in-scope, profile-appropriate consent
PRI-001 — Privacy Policy AUTO Retrieved open banking data is used only for the purpose declared at consent and is deleted after the consuming workflow (migration, affordability assessment, or account pre-fill) completes — purpose-binding is enforced per jurisdiction profile requirements

MOD-114 — Direct debit mandate management

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

What it does

MOD-114 manages the mandate registry and processing pipeline for direct debits in both NZ and AU. The module is a thin integration layer — the actual payment rail (NZ Payments NZ DDR scheme; AU BECS via sponsor bank) is operated by an external provider. MOD-114 owns: mandate storage, mandate validation at debit presentation time, dishonour processing, customer-initiated cancellation, and the dispute workflow for unauthorised debits.

External rail integration

NZ DDR (Direct Debit Register) is administered by Payments NZ; AU BECS is administered by AusPayNet via a sponsor/correspondent bank. The bank connects to these rails via API (not direct scheme membership at launch). The integration partner handles scheme connectivity; MOD-114 provides the mandate validation and ledger instruction layer.

Mandate lifecycle

Creation: Customer authorises a biller mandate via the bank's app (pre-approval flow) or a biller-initiated paper/electronic authorisation processed by the bank. Mandate is stored with: biller name, biller ID, account number, maximum amount per debit (if customer-specified), frequency, effective date, expiry date (if applicable).

Validation at presentation: When a debit file is received from the external provider, each debit entry is validated: mandate exists and is active, biller ID matches, amount does not exceed the mandate maximum (if set), account is in Active state (MOD-007). Debits failing validation are returned with the appropriate scheme return code.

Dishonour: If the account has insufficient funds or is blocked, the debit is returned with a dishonour code. A dishonour fee is assessed via MOD-110 (if configured in tenant fee schedule). The customer is notified via MOD-063 with the biller name and dishonour reason.

Cancellation: Customer can cancel any mandate instantly via the app — effective immediately; any debit presentation received after cancellation is returned regardless of timing. Cancellation is logged and the external provider is notified.

Dispute: Customer can dispute a direct debit within 12 months of the debit date (NZ/AU consumer protection periods). Dispute triggers MOD-053 (case and complaint management) and places a query hold on repeat debits from the same biller pending investigation.

Data model

-- payments.direct_debit_mandates (Postgres)
CREATE TABLE payments.direct_debit_mandates (
  mandate_id   uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  party_id     uuid NOT NULL,
  account_id   uuid NOT NULL,
  jurisdiction text NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  biller_id    text NOT NULL,
  biller_name  text NOT NULL,
  scheme_ref   text,               -- DDR ref (NZ) or APCA ID (AU)
  max_amount   numeric(18,2),      -- null = no customer-set maximum
  frequency    text,               -- WEEKLY | FORTNIGHTLY | MONTHLY | VARIABLE
  status       text NOT NULL CHECK (status IN ('ACTIVE','CANCELLED','SUSPENDED','EXPIRED')),
  created_at   timestamptz NOT NULL DEFAULT now(),
  cancelled_at timestamptz,
  cancel_reason text
);

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE A direct debit is only processed against an account if a valid active mandate exists for the requesting biller — no mandate, no debit.
PAY-002 — Settlement Risk Policy LOG All mandate lifecycle events and debit presentations are logged immutably in payments.direct_debit_events, providing the settlement risk and scheme-rule audit trail required by NZ Payments NZ DDR and AU BECS.
PRI-001 — Privacy Policy GATE Mandate data (biller name, account reference) is stored and accessible only to the account holder and authorised bank staff — not shared with third parties beyond the payment rail.

MOD-119 — BPAY payment integration

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

MOD-119 provides BPAY payment capability for Australian customers. BPAY is the primary bill payment infrastructure in Australia, used to pay utilities, insurance, council rates, tax obligations, and other registered billers. The module handles BPAY payment initiation (payer side), biller directory lookup, settlement batch participation, and returns processing. It operates as a participant in the BPAY scheme via the bank's sponsor banking relationship (ADR-005).

Jurisdiction

Australia only. The module is inactive for NZ-jurisdiction accounts. Feature flag: payments.bpay.enabled — set at the tenant level.

Compliance rationale

The AU ePayments Code governs BPAY transactions, particularly around liability for mistaken payments, unauthorised transactions, and processing error remediation. The BPAY scheme rules (operated by BPAY Pty Ltd) impose obligations on participant banks regarding settlement times, dishonour handling, and dispute resolution timelines. PAY-009 (Payment Exceptions, Returns & Reversals Policy) applies directly to BPAY dishonour and return events.

Commercial rationale

BPAY is a table-stakes capability for any Australian retail bank. Customers use BPAY to pay bills from within their banking app — inability to make BPAY payments renders an account unsuitable as a primary bank account. Without BPAY, every prospective AU customer would need to maintain a separate bill payment arrangement, which is a UX regression and a material competitive disadvantage.

Data model

-- payments.bpay_payments
CREATE TABLE payments.bpay_payments (
  bpay_payment_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  biller_code        TEXT NOT NULL,
  biller_name        TEXT,  -- resolved from biller directory
  customer_reference TEXT NOT NULL,
  amount             NUMERIC(18,2) NOT NULL,
  currency           TEXT NOT NULL DEFAULT 'AUD',
  payment_date       DATE NOT NULL,  -- value date
  status             TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','submitted','settled','returned','disputed')),
  batch_id           TEXT,  -- BPAY settlement batch reference
  sponsor_reference  TEXT,  -- sponsor bank submission reference
  returned_at        TIMESTAMPTZ,
  return_reason_code TEXT,
  posting_id         UUID,  -- reference to MOD-001 entry
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- payments.bpay_biller_cache
CREATE TABLE payments.bpay_biller_cache (
  biller_code        TEXT PRIMARY KEY,
  biller_name        TEXT NOT NULL,
  accepted_amounts   TEXT NOT NULL CHECK (accepted_amounts IN ('fixed','variable','range')),
  min_amount         NUMERIC(18,2),
  max_amount         NUMERIC(18,2),
  crn_format         TEXT,  -- regex for CRN validation
  last_refreshed     TIMESTAMPTZ NOT NULL
);

Key operations

1. Biller lookup

Customer enters a biller code in the app. The module queries bpay_biller_cache, which is refreshed daily from the BPAY biller directory API via the sponsor bank. The response returns the biller name and CRN format for client-side validation. If the biller code is not present in cache, a real-time lookup is made via the sponsor bank API with a 5-second timeout; failure results in an inline error prompting the customer to retry.

2. CRN validation

The customer reference number is validated against the biller's published CRN format — either a Luhn check or a regex pattern as specified by the biller record. An invalid CRN blocks submission with an inline error before any funds are committed or any network call is made to the scheme.

3. Payment submission

On customer confirmation:

  1. MOD-020 pre-flight checks — balance, sanctions screening, daily payment limits.
  2. MOD-023 fraud score — BPAY-specific rules applied.
  3. Debit account via MOD-001 (double-entry).
  4. Submit to sponsor bank BPAY API; record sponsor_reference and set status to submitted.

Payments submitted before the configured cut-off (default: 17:00 AEST) are processed same-day. Payments submitted after cut-off are queued for the next business day. The cut-off time is configurable at the tenant level.

4. Settlement

The sponsor bank confirms settlement batch inclusion. The module updates status to settled and records the batch_id. MOD-081 reconciles the batch against the sponsor bank settlement statement.

5. Returns and dishonours

Returned payments received from the sponsor bank (e.g. invalid biller code, biller rejection, account closed at biller side) trigger the following:

  1. Credit account via MOD-001.
  2. Set status to returned and record return_reason_code.
  3. Dispatch notification via MOD-063 with the return reason expressed in plain language (raw scheme reason codes are mapped to a customer-facing message catalogue).

6. Dispute handling

When a customer disputes a BPAY payment (wrong biller, wrong amount, not authorised), a case is created in MOD-053 (case management) with case type bpay_dispute. The dispute is submitted to the sponsor bank via the BPAY dispute process. The scheme allows a 90-day dispute window from the payment date.

7. Transaction history

BPAY transactions surface in MOD-070 (transaction history) enriched with biller name (resolved from biller directory, not raw biller code), amount, payment date, and CRN. A status badge indicates Pending, Settled, or Returned.

UX requirements

The BPAY payment flow in MOD-071 (payment initiation) must implement:

  • Biller code entry with real-time biller name resolution.
  • CRN entry with format mask derived from biller CRN format.
  • Amount entry, or pre-populated fixed amount where biller data specifies a fixed amount.
  • Scheduled date picker — pay now or select a future date.
  • Review screen showing biller name, CRN, amount, and value date before confirmation.
  • Confirm step with biometric gate (same as standard payment flow).
  • Recent billers list for quick re-access — stored per customer, shown on the BPAY entry screen.

Requirements satisfied

FR-537 — Biller lookup and CRN validation. FR-538 — BPAY payment submission and cut-off handling. FR-539 — Settlement batch participation and reconciliation. FR-540 — Returns and dishonour processing.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE BPAY payments are validated against biller code, customer reference number format, and payment amount limits before submission to the BPAY scheme.
PAY-005 — Payment Fraud Prevention Policy AUTO BPAY transactions are passed through the transaction fraud scorer before submission; high-risk transactions are held for review.
PAY-009 — Payment Exceptions, Returns & Reversals Policy AUTO BPAY dishonours and returns are handled automatically — funds are credited back to the customer's account and a notification is dispatched.
CON-005 — Fee & Pricing Transparency Policy AUTO BPAY biller name and reference are displayed in transaction history with full detail — no generic merchant string.
REP-005 — Data Quality & Assurance Policy LOG All BPAY transactions are logged with biller code, customer reference, and settlement batch reference for data quality and reconciliation purposes.

MOD-120 — PayID and Osko integration

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

MOD-120 provides PayID registration and management, and Osko payment initiation and receipt via Australia's New Payments Platform (NPP). PayID allows customers to receive money using a proxy identifier — mobile number, email address, or ABN — instead of BSB and account number. Osko is the real-time overlay service that enables instant account-to-account transfers 24/7/365 with a 15-second clearing target.

Jurisdiction

Australia only. The module is inactive for NZ-jurisdiction accounts. Feature flag: payments.osko.enabled. Operates through the bank's sponsor banking relationship which provides NPP connectivity (ADR-005).

Compliance rationale

NPP Rules (au-npp-rules) govern PayID management — including portability, disputes, and cancellation — and Osko payment obligations. The AU ePayments Code applies to unauthorised Osko transactions and error correction. The Scam-Safe Accord (au-scam-safe-accord) imposes specific obligations around confirmation of payee: the name confirmation step in the outbound payment flow is a direct Scam-Safe Accord requirement. ASIC and AFCA have increasing supervisory focus on NPP fraud, particularly Authorised Push Payment (APP) scams. The friction and warning measures applied to high-value first-time payees are designed to reduce APP scam exposure.

Commercial rationale

PayID and Osko are expected features of any Australian bank account. Customers want to be findable via their phone number and to send money instantly. The inability to receive a PayID makes the bank invisible to NPP-sending institutions — senders see the recipient's bank as "not NPP-enabled" and may be prompted to use alternative transfer methods. Osko also underpins instant merchant settlement use cases planned for future product tiers.

Data model

-- payments.payid_registrations
CREATE TABLE payments.payid_registrations (
  payid_id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  payid_type         TEXT NOT NULL CHECK (payid_type IN ('mobile','email','abn','organisation_id')),
  payid_value        TEXT NOT NULL,
  display_name       TEXT NOT NULL,  -- shown to senders
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','suspended','deregistered')),
  registered_at      TIMESTAMPTZ NOT NULL,
  deregistered_at    TIMESTAMPTZ,
  ported_from_bsb    TEXT,  -- if ported from another bank
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (payid_type, payid_value)
);

-- payments.osko_payments
CREATE TABLE payments.osko_payments (
  osko_payment_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  direction          TEXT NOT NULL CHECK (direction IN ('outbound','inbound')),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  payid_used         TEXT,  -- null if BSB/account used directly
  payid_type         TEXT,
  resolved_bsb       TEXT,
  resolved_account   TEXT,
  beneficiary_name   TEXT NOT NULL,
  amount             NUMERIC(18,2) NOT NULL,
  currency           TEXT NOT NULL DEFAULT 'AUD',
  description        TEXT,
  end_to_end_id      TEXT NOT NULL UNIQUE,
  npp_message_id     TEXT,
  status             TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','processing','completed','returned','disputed')),
  submitted_at       TIMESTAMPTZ,
  completed_at       TIMESTAMPTZ,
  returned_at        TIMESTAMPTZ,
  return_reason      TEXT,
  posting_id         UUID,
  name_confirmed     BOOLEAN NOT NULL DEFAULT false,
  name_confirmed_at  TIMESTAMPTZ,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

PayID lifecycle

Registration at account opening

When a customer opens PRD-001 (everyday account), they are prompted to register their mobile number and/or email address as a PayID. Default behaviour is opt-in. Registrations are stored in payid_registrations and provisioned to the NPP directory via the sponsor bank API.

PayID management

Customers can add a new PayID, update the display name shown to senders, suspend a PayID temporarily, or deregister via MOD-072 (customer profile and settings). Deregistration is immediate — the PayID is no longer resolvable within 2 minutes of the instruction, per NPP Rule 4.7.

PayID portability

When a customer migrating from another institution wants to bring an existing PayID: the module initiates a port request via the sponsor bank. The current PayID holder at the source institution has 5 business days to raise a dispute. If no dispute is lodged, the port completes and ported_from_bsb records the source institution's BSB.

Osko payment send flow

  1. Customer enters a PayID value — or BSB and account number — in MOD-071 (payment initiation).
  2. The module resolves the PayID to account details via the NPP directory in real time, with a 2-second SLA. The beneficiary display name is returned.
  3. Name confirmation — the resolved display name is shown to the customer: "You are paying [Display Name]." The customer must explicitly confirm. name_confirmed = true is set before any funds are committed or any network submission is made. This step is not skippable. It is a Scam-Safe Accord requirement.
  4. MOD-020 pre-payment validation → MOD-023 fraud score. Enhanced fraud rules apply to Osko: if the recipient has not previously been paid by this customer and the amount exceeds $1,000, additional friction is applied — a 5-second countdown with a scam awareness warning before the confirm button activates.
  5. Post debit via MOD-001 → submit NPP message via sponsor bank → record npp_message_id and set status to processing.
  6. Completion notification dispatched via MOD-063 within 30 seconds of submission.

Inbound Osko receipt

  1. Sponsor bank delivers an inbound NPP credit notification to the module.
  2. The module resolves the destination account via PayID lookup or direct BSB/account match.
  3. Credit posted via MOD-001 immediately.
  4. Push notification dispatched via MOD-063 within 5 seconds: "[Sender name] sent you $[amount]".

Returns handling

If an outbound Osko payment is returned (account closed, PayID deregistered between lookup and settlement, scheme rejection):

  1. Credit account via MOD-001.
  2. Set status to returned and record return_reason.
  3. Notify customer via MOD-063 with return reason expressed in plain English. NPP scheme return reason codes are mapped to a customer-facing message catalogue.

Scam-Safe Accord obligations

The module implements the following specific obligations from the Scam-Safe Accord:

  • Name confirmation — mandatory payee name display and acknowledgement before every outbound Osko payment (implemented in the send flow above).
  • High-value first-time payee friction — delay and scam warning for transfers above $1,000 to a new payee.
  • In-app scam reporting — a "Report a scam" action is available on every Osko transaction detail screen; this routes to MOD-053 (case management) with case type scam_report.
  • Onboarding education — scam awareness content is shown to the customer during their first session with the Osko payment flow, before they submit their first payment.

Requirements satisfied

FR-541 — PayID registration and management, including portability. FR-542 — Osko payment initiation with name confirmation and fraud friction. FR-543 — Inbound Osko receipt and real-time credit posting. FR-544 — Osko returns processing and Scam-Safe Accord obligations.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE PayID-addressed payments are validated for PayID existence and account reachability before funds are committed.
PAY-005 — Payment Fraud Prevention Policy AUTO All Osko payment initiations pass through the transaction fraud scorer with additional real-time rules for new-payee high-value transfers.
PAY-009 — Payment Exceptions, Returns & Reversals Policy AUTO Osko payment returns (to wrong PayID, account closed) are processed automatically with immediate customer notification and funds reversal.
CON-005 — Fee & Pricing Transparency Policy AUTO The resolved account holder name is displayed to the customer before confirming an Osko payment — confirmation of payee name is mandatory.
AML-005 — Transaction Monitoring Policy LOG All real-time Osko payments are logged with full transaction metadata for transaction monitoring purposes.

MOD-122 — NZ faster payments and A2A integration

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

Provides NZ interbank payment capability via the Payments NZ clearing framework. Handles outbound Account2Account (A2A) credit transfers, inbound credit receipts, settlement batch participation, and return payment processing. Operates under the Payments NZ rules and through the bank's sponsor banking relationship (ADR-005). NZ only — AU interbank payments (NPP/Osko) are handled by MOD-120.

Jurisdiction: New Zealand only. Feature flag: payments.nz_interbank.enabled.

Compliance rationale

Payments NZ Rules (nz-payments-nz-rules) govern interbank credit transfers, including same-day settlement obligations, return payment handling, and participant conduct. PAY-002 (Settlement Risk Policy) requires that settlement exposure is monitored and that the bank participates in the Payments NZ settlement model via its sponsor bank. The Payments NZ Rules also impose obligations on banks to credit inbound payments promptly — typically within 2 hours of receipt during business hours.

Commercial rationale

NZ interbank transfers are the foundation of everyday banking. Customers need to send money to accounts at other NZ banks — to pay rent, pay tradespeople, pay family members. Without NZ interbank capability, the platform cannot serve as a primary bank account in New Zealand.

Payment types supported

  • Standard credit transfer (BSB-equivalent in NZ: bank code + branch code + account number + suffix)
  • Batch credit (multiple credits to different accounts in one submission — for SME payroll)
  • Same-day settlement (for payments submitted before the daily cut-off)

Data model

-- payments.nz_interbank_payments
CREATE TABLE payments.nz_interbank_payments (
  payment_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  direction          TEXT NOT NULL CHECK (direction IN ('outbound','inbound')),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  beneficiary_bank   TEXT NOT NULL,  -- NZ bank code
  beneficiary_branch TEXT NOT NULL,
  beneficiary_account TEXT NOT NULL,
  beneficiary_suffix TEXT,
  beneficiary_name   TEXT NOT NULL,
  amount             NUMERIC(18,2) NOT NULL,
  currency           TEXT NOT NULL DEFAULT 'NZD',
  particulars        TEXT,  -- NZ payment reference fields
  code               TEXT,
  reference          TEXT,
  payment_date       DATE NOT NULL,
  status             TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','submitted','cleared','settled','returned','disputed')),
  clearing_reference TEXT,  -- Payments NZ clearing reference
  settlement_batch   TEXT,
  returned_at        TIMESTAMPTZ,
  return_reason_code TEXT,
  posting_id         UUID,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

Key operations

1. Outbound payment

Customer enters bank/branch/account/suffix in payment initiation (MOD-071). System validates format using the NZ account number validation algorithm. MOD-020 pre-payment validation runs → MOD-023 fraud score applied → debit posted via MOD-001 → submitted to sponsor bank API → clearing_reference recorded. Payments submitted before the daily cut-off (typically 3pm NZST) settle same day. Payments submitted after cut-off settle the next business day.

2. Inbound payment

Sponsor bank delivers an inbound credit notification. Module resolves the notification to the destination account. Credit posted via MOD-001. Customer notified via MOD-063 within 60 minutes of receipt during business hours, meeting the Payments NZ obligation for prompt crediting.

3. Returns

If an outbound payment returns (invalid account, account closed, beneficiary bank reject): credit posted back to the originating account via MOD-001, status set to returned, notification dispatched via MOD-063 with the return reason expressed in plain language.

4. Settlement reconciliation

Daily: MOD-081 reconciles the settlement batch against the sponsor bank's settlement statement. Any discrepancies are flagged to operations for investigation.

5. NZ account number format

NZ accounts use the format XX-XXXX-XXXXXXX-XX (bank-branch-account-suffix). This module validates format and performs the Payments NZ account number validation checksum before any submission to the clearing network.

Requirements

FR-553 through FR-556.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE All outbound NZ interbank payments pass pre-payment validation (balance, sanctions, daily limits) before submission to the Payments NZ clearing network.
PAY-002 — Settlement Risk Policy LOG Every NZ interbank payment instruction, clearing confirmation, and settlement batch entry is logged to the payment audit trail for settlement risk management purposes.
PAY-005 — Payment Fraud Prevention Policy AUTO NZ interbank payments pass through the transaction fraud scorer before submission; high-risk payments trigger a review hold.
PAY-009 — Payment Exceptions, Returns & Reversals Policy AUTO Returned payments (invalid account, account closed) are credited back to the originating account automatically with a customer notification.
REP-005 — Data Quality & Assurance Policy LOG All interbank payment data is logged with Payments NZ clearing references for data quality and reconciliation purposes.

MOD-123 — ATM network integration

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

Handles real-time ATM withdrawal authorisation, ATM transaction posting, and ATM network settlement reconciliation. Processes inbound authorisation requests from the card network (via the sponsor bank's ATM network connectivity), validates them against balance and card controls, and responds within the network's required response time (typically under 1 second). NZ: connects to shared ATM networks (Westpac/BNZ/ANZ sharing arrangements under the Payments NZ framework). AU: connects to the rediATM network and major bank surcharge-free agreements.

Prerequisite: Physical card issuance (MOD-124) must be operational. ATM transactions require a physical card with a valid PAN enrolled in the network.

Compliance rationale

The ePayments Code (AU) and Banking Code obligations require banks to respond promptly to disputed ATM transactions and to correctly attribute liability for card-not-present vs. card-present fraud. PCI DSS (PAY-006) governs how card data is handled during authorisation — the platform must use point-to-point encryption and must never log or store full PANs in application logs.

Data model

-- payments.atm_authorisations
CREATE TABLE payments.atm_authorisations (
  auth_id            UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  card_id            UUID NOT NULL,
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  atm_id             TEXT NOT NULL,  -- terminal ID from network message
  atm_location       TEXT,
  network            TEXT NOT NULL,  -- 'nz_eftpos','visa','mastercard','redi'
  amount             NUMERIC(18,2) NOT NULL,
  currency           TEXT NOT NULL,
  request_received_at TIMESTAMPTZ NOT NULL,
  decision           TEXT NOT NULL CHECK (decision IN ('approved','declined','reversed')),
  decline_reason     TEXT,
  response_sent_at   TIMESTAMPTZ,
  response_time_ms   INT,  -- for SLA monitoring
  posting_id         UUID,
  fraud_score        NUMERIC(5,2),
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- payments.atm_settlements
CREATE TABLE payments.atm_settlements (
  settlement_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  settlement_date    DATE NOT NULL,
  network            TEXT NOT NULL,
  batch_reference    TEXT NOT NULL,
  total_debits       NUMERIC(18,2) NOT NULL,
  total_credits      NUMERIC(18,2) NOT NULL,
  reconciled         BOOLEAN NOT NULL DEFAULT false,
  reconciled_at      TIMESTAMPTZ,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

Key operations

1. Authorisation flow

An inbound ATM request arrives via the sponsor bank's network interface. The module looks up the card → account → available balance (MOD-003). Checks performed: ATM toggle enabled (MOD-078), daily ATM limit not exceeded (MOD-021), fraud score acceptable (MOD-023). An approved or declined response is returned within an 800ms SLA. If approved, a pre-authorisation hold (pre-auth debit) is posted via MOD-001 immediately to reduce available balance; the hold is finalised on settlement.

2. Reversal

If an ATM dispenses no cash but an authorisation was approved (dispense failure): an inbound reversal message triggers release of the hold via MOD-001. The customer is notified via MOD-063 if the reversal occurs unexpectedly.

3. Settlement

Daily: the sponsor bank delivers an ATM settlement file. MOD-081 reconciles posted ATM transactions against the settlement file. Discrepancies are flagged to operations for investigation.

4. Surcharge disclosure

The app displays surcharge-free ATM networks in the card section (MOD-078 UI). Customers can locate surcharge-free ATMs in-app before making a withdrawal.

Requirements

FR-557 through FR-560.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE ATM withdrawal requests are validated in real time against the customer's available balance and daily ATM limit before authorisation is returned to the network.
PAY-005 — Payment Fraud Prevention Policy AUTO ATM withdrawal requests are screened by the transaction fraud scorer using device and location signals; anomalous requests trigger a decline or step-up challenge.
PAY-002 — Settlement Risk Policy LOG All ATM authorisation decisions, reversals, and settlement entries are logged to the payment audit trail.
CON-005 — Fee & Pricing Transparency Policy AUTO Surcharge-free network membership details are disclosed to the customer in the app so they know which ATMs can be used without fees.

MOD-124 — Physical card issuance and bureau integration

System: SD04 | Repo: bank-payments | Build status: Built | Deployed: No

Purpose

Manages the full lifecycle of physical debit cards: ordering cards from the card bureau at account opening, handling replacements and renewals, card activation, PIN management, and card inventory tracking. Integrates with a card bureau (e.g. IDEMIA, Thales, ABnote) via their standard API for personalisation file submission and order tracking. Physical cards operate on the Visa or Mastercard network per the sponsor bank's card scheme membership.

PCI DSS scope

This module is in PCI DSS scope. Card personalisation data (PAN, expiry, CVV) must be handled per PCI DSS v4.0 requirements. Full PANs are never logged. Data in transit uses TLS 1.2+. The bureau API connection uses mTLS. PIN operations use the bank's Hardware Security Module (HSM) — PIN blocks are generated on the customer's device using the HSM's public key and decrypted only within the HSM. The module never touches a plaintext PIN.

Card lifecycle

ORDERED → PRODUCED → DISPATCHED → ACTIVE → FROZEN → CANCELLED

Parallel states: EXPIRED (triggers renewal 60 days before expiry date), LOST_STOLEN (triggers immediate cancellation and replacement order).

Data model

-- payments.physical_cards
CREATE TABLE payments.physical_cards (
  card_id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id          UUID NOT NULL,                 -- cross-domain ref to SD01 accounts.accounts(id); application-layer only (no DB FK — cross-DB)
  customer_id         UUID NOT NULL,                 -- cross-domain ref to SD02 party.parties(party_id)
  card_type           TEXT NOT NULL DEFAULT 'debit'
                        CHECK (card_type IN ('debit','prepaid','credit')),
                      -- 'credit' added per ADR-058. Credit cards link to the SD05
                      -- credit facility engine (MOD-167) via credit_facility_id below.
  credit_facility_id  UUID,                          -- NULL for debit/prepaid. For credit cards,
                                                     -- application-layer ref to future MOD-167
                                                     -- credit_card_facilities(id). No DB FK —
                                                     -- cross-schema; set at card issuance.
  bin_range_type      TEXT NOT NULL DEFAULT 'sponsor'
                        CHECK (bin_range_type IN ('sponsor','principal')),
                      -- 'sponsor' = BIN held via sponsor bank (standard at launch per ADR-005).
                      -- 'principal' = direct scheme membership (Phase 4 option, deferred).
  scheme              TEXT NOT NULL CHECK (scheme IN ('visa','mastercard')),
  pan_last4           TEXT NOT NULL,                 -- full PAN never stored in Neon (PAY-006 / ADR-058)
  expiry_month        INT NOT NULL,
  expiry_year         INT NOT NULL,
  status              TEXT NOT NULL DEFAULT 'ordered'
                        CHECK (status IN ('ordered','produced','dispatched','active','frozen','cancelled','expired')),
  order_reference     TEXT,                          -- bureau order ID
  dispatch_tracking   TEXT,                          -- courier tracking number
  activated_at        TIMESTAMPTZ,
  cancelled_at        TIMESTAMPTZ,
  cancellation_reason TEXT,                          -- 'lost','stolen','expired','customer_request'
  replacement_for     UUID,                          -- previous card_id if this is a replacement
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

Credit-ready columns (ADR-058): credit_facility_id and bin_range_type are included in the v1 schema even though no credit card product exists at launch. Adding them pre-build is a zero-cost change; adding them post-launch would require a production migration against a live card table. credit_facility_id is NULL for all debit and prepaid cards. When a credit card product is launched (Phase 2 per ADR-058), it is populated with the MOD-167 facility ID at card issuance time.

Key operations

1. New card order

Triggered at account opening or on customer request. The module generates personalisation data (name, PAN, expiry — PAN generated per scheme rules, CVV computed via HSM). Submits the personalisation file to the bureau API and records the order_reference. MOD-063 notifies the customer that their card has been ordered with an expected delivery window of 5–7 business days.

2. Card activation

The customer activates via the app (tap "Activate card" → biometric confirmation). The module calls the bureau to activate the PAN in the network and updates status to active. MOD-063 confirms activation. The card is simultaneously enrolled in Apple Pay and Google Pay via a tokenisation request to the card scheme.

3. PIN set

The customer sets their PIN via the in-app PIN pad. The PIN is encrypted on-device using the HSM's public key. The encrypted PIN block is sent to the HSM service, which translates it to a network PIN block for submission to the card scheme's PIN management network. The PIN is never known to the platform at any point.

4. Lost or stolen

The customer reports the card lost or stolen via the app or back office. The module immediately sets status to cancelled and calls MOD-078 to freeze all card transactions. A replacement card order is simultaneously created (new PAN, same account). The previous card is blocked at the network via the sponsor bank.

5. Renewal

60 days before card expiry a replacement card is auto-ordered. The new card is dispatched to the customer. The old card is cancelled on the expiry date. The customer is notified at 60 days and again on dispatch.

Requirements

FR-561 through FR-564.

Policies satisfied:

Policy Mode Description
PAY-003 — Card Scheme Compliance Policy GATE Physical cards are produced only after passing card scheme compliance checks — the card personalisation file conforms to Visa/Mastercard scheme specifications before submission to the bureau.
PAY-006 — PCI DSS Compliance Policy AUTO Card personalisation data is transmitted to the bureau using point-to-point encryption; full PANs are never stored in application logs or databases outside of PCI DSS-compliant storage.
DT-001 — Information Security Policy GATE PIN set and PIN change operations forward encrypted PIN blocks to the processor's HSM via MOD-124's PIN management abstraction — PINs are never in plaintext within bank systems at any point in the processing chain.
CON-001 — Customer Fairness & Conduct Policy AUTO Card replacement is initiated automatically when a card is reported lost or stolen, with no manual intervention required to trigger the bureau order.

MOD-135 — Batch payment and payroll file processing

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

Batch payment and payroll file processing allows business and SME customers to upload a structured payment file and initiate multiple outbound payments in a single operation. The source account is debited for the total settled amount; each individual beneficiary receives a separate credit. This is the primary mechanism for payroll runs, creditor payment runs, and supplier disbursements.

Supported file formats

ABA (Australian Bankers Association)

The ABA format is the standard for Australian payroll and creditor payment files. The file structure is fixed-width with a descriptive header record (Type 0), detail records (Type 1), and a file total record (Type 7).

Key validation requirements: - BSB must be in the format NNN-NNN and must exist in the BSB directory - Account numbers are 1–9 digits, right-justified, zero-padded - Transaction codes must be within the permitted set (13, 50, 51, 52, 53, 54, 55, 56, 57) - The Type 7 hash total must equal the sum of all BSB digits from detail records (standard ABA hash); mismatch is a hard rejection - Amounts are in cents (integer); no decimal point in the file

CSV (NZ and AU)

A CSV format is accepted for both NZ and AU customers. The column specification is fixed and must be present in the header row in this order:

Column Description Format
beneficiary_account Account number or IBAN (AU: BSB+account, NZ: full 16-digit) TEXT
beneficiary_name Name of receiving party TEXT ≤ 32 chars
amount Payment amount DECIMAL(12,2)
reference Payment reference visible to beneficiary TEXT ≤ 12 chars
particulars Additional detail (NZ only; ignored for AU) TEXT ≤ 12 chars

Files with a missing or mis-ordered header row are rejected at format validation.

Validation rules

Format validation is applied before the file is accepted into the processing queue. Any file that fails format validation is rejected in full with a structured error response identifying the failing row number and field name. Partial processing of a format-invalid file is not permitted.

Validation checks, in order:

  1. File format detection — file extension and header row determine format (ABA or CSV); ambiguous files are rejected
  2. Encoding — UTF-8 required; BOM is accepted and stripped
  3. Row count — must be at least 1 detail record; files with zero payment rows are rejected
  4. Account number format — per-jurisdiction: AU BSB+account validated against the BSB directory; NZ 16-digit format validated against the bank/branch prefix list
  5. Amount range — each item amount must be greater than zero and within the per-transaction limit set on the source account; items exceeding the per-transaction limit are flagged as validation errors, not quarantined
  6. ABA hash total — checked for ABA files (see above)
  7. CSV item count — row count in the body must match the value declared in the CSV header (if present)

Processing model

The platform uses a best-efforts model, not all-or-nothing. This matches the behaviour of the BECS clearing system in AU and the direct credit interchange in NZ.

  • The source account is debited for the total amount of items that pass validation and are successfully submitted
  • Items that fail validation are rejected before submission and are not charged
  • Items that generate an AML screening alert are quarantined after submission; the amount for quarantined items is not debited until the alert is resolved
  • Items where the credit cannot be posted (account closed, invalid account number) generate a return; the return credit is applied to the source account within the same business day

The customer sees a clear distinction between: items rejected at validation (never charged), items quarantined at screening (charged pending resolution), and items returned after submission (credited back).

Data model

-- One record per uploaded batch file
CREATE TABLE payments.batch_files (
    batch_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    account_id      UUID NOT NULL REFERENCES core.accounts(account_id),
    file_format     TEXT NOT NULL CHECK (file_format IN ('aba', 'csv')),
    item_count      INTEGER NOT NULL,
    total_amount    NUMERIC(15, 2) NOT NULL,
    status          TEXT NOT NULL CHECK (status IN (
                        'uploaded',
                        'validating',
                        'pending_approval',
                        'processing',
                        'settled',
                        'failed'
                    )),
    submitted_at    TIMESTAMPTZ,
    settled_at      TIMESTAMPTZ,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- One record per payment item within a batch
CREATE TABLE payments.batch_items (
    item_id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    batch_id            UUID NOT NULL REFERENCES payments.batch_files(batch_id),
    beneficiary_account TEXT NOT NULL,
    beneficiary_name    TEXT NOT NULL,
    amount              NUMERIC(12, 2) NOT NULL,
    reference           TEXT,
    status              TEXT NOT NULL CHECK (status IN (
                            'pending',
                            'submitted',
                            'settled',
                            'failed',
                            'quarantined'
                        )),
    failure_reason      TEXT,           -- populated for failed and quarantined items
    posting_id          UUID,           -- references the MOD-001 posting once settled
    created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

status on batch_files follows the lifecycle: uploadedvalidatingpending_approval (customer confirmation step) → processingsettled or failed. Individual batch_items have their own status that progresses independently after the batch moves to processing.

Key operations

Upload and validate

The customer uploads a file via the app or API. The module detects the format, runs the full validation sequence, and either accepts the file (status: pending_approval) or rejects it with a structured error list. Validation errors are returned with row numbers and field names so the customer can correct and re-upload.

Customer confirmation

Before any payment is processed, the customer is presented with a confirmation screen showing: total number of items, total amount, source account, and — where format validation identified any issues — a summary of rejected items. The customer must explicitly confirm before the batch moves to processing. No batch is processed silently.

AML screening pass

Once the customer confirms, each item is passed through the transaction screening engine (MOD-023) and the AML transaction screening engine individually. Items that generate no alert move to submitted. Items that generate an alert move to quarantined with the reason recorded in failure_reason. The remaining non-alerted items continue to submission without waiting for quarantined items to be resolved. The customer is notified of quarantined items within 60 seconds of submission via MOD-063.

Submission and settlement

Validated, non-quarantined items are submitted to the clearing system. The source account is debited for the total of submitted items via MOD-001. Each credit is posted as a separate atomic transaction. The batch status moves to settled when all non-quarantined items have a final status (settled, failed, or returned).

Failure handling

Items that cannot be credited (account closed, account number not found, returned by the receiving bank) generate a return. The return triggers a re-credit to the source account via MOD-001 within the same business day. The batch_items record is updated to failed with the return reason in failure_reason. The customer is notified of returns via MOD-063.

Batch-level failures (e.g. source account closed mid-batch, system error during processing) move the batch to failed. Items not yet submitted at the point of batch failure are not charged.

Settlement reconciliation

End-of-day reconciliation for batch files runs via the payment reconciliation engine (MOD-081). The reconciliation confirms that the sum of settled batch_items amounts matches the clearing settlement file and that the source account debit matches the total of posted credits plus any outstanding returns.

Requirements

FR-601 — System shall validate each batch file against the declared format specification (ABA field positions and hash total for AU; CSV column spec and row count for NZ/AU) before accepting the file, rejecting files that fail format validation with a structured error identifying the failing row or field, and must not partially process a file that fails format validation.

FR-602 — System shall check the source account's available balance against the total batch amount via MOD-020 before processing any items; if funds are insufficient for the full batch, the system must present the shortfall to the customer and must not process the batch until the customer confirms they want to proceed with partial funding or until sufficient funds are available.

FR-603 — System shall screen each batch item through the transaction fraud scorer (MOD-023) and the AML transaction screening engine independently; items that generate a screening alert must be quarantined with status quarantined and reason recorded; quarantined items must not block non-alerted items from being submitted; the customer must be notified of any quarantined items within 60 seconds of the batch being submitted.

FR-604 — System shall post the source account debit and each individual beneficiary credit via MOD-001 as separate atomic transactions — the source debit for the total settled amount and each credit must both succeed or both fail; a credit that cannot be posted (account closed, invalid account) must be returned and the corresponding source account amount re-credited within the same business day.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE Source account available balance is checked against the total batch amount before any item is processed; if funds are insufficient the batch is held and the customer notified.
AML-007 — Sanctions Screening Policy AUTO Each payment item in the batch passes through the transaction screening engine before being submitted; items that generate a screening alert are quarantined and the batch continues with remaining items.
PAY-002 — Settlement Risk Policy LOG Every batch file, every item within it, and every submission outcome is logged to the payment audit trail.
CON-005 — Fee & Pricing Transparency Policy AUTO Customers uploading a batch must confirm the total amount and payment count before submission — no silent batch processing.

MOD-136 — BPAY biller registration and inbound BPAY

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

MOD-136 enables the bank's business customers to be registered as BPAY billers so that members of the public can pay them from any Australian bank using a BPAY biller code and customer reference number (CRN). The inbound payment flows through the BPAY scheme via the bank's sponsor bank relationship, arrives as part of a daily settlement file, and is credited automatically to the registered biller's nominated account. This is the complement to MOD-119 (outbound BPAY): MOD-119 handles customers paying bills; MOD-136 handles customers receiving bill payments.

Scope

Australia only. The module is inactive by default and is enabled via the payments.bpay.inbound.enabled feature flag at the tenant level. NZ customers do not have access to this feature; BPAY is an Australian-only payment scheme.

BPAY biller registration process

Registration is a back-office–initiated workflow. To register a business customer as a BPAY biller:

  1. The back-office team collects the required information from the customer: entity name, nominated account (the account to which inbound payments are credited), CRN format specification (Luhn, regex, or length-only), and the desired biller name as it will appear on payers' bank statements.
  2. A biller registration request is submitted to the sponsor bank, including the biller name, nominated account BSB and account number, and CRN validation configuration.
  3. The sponsor bank registers the biller with BPAY Pty Ltd. This process typically takes 3–5 business days from submission.
  4. Once the sponsor bank returns the assigned biller code, the payments.bpay_billers record is updated from pending_registration to active and the biller code is recorded.

No inbound payments may be credited to a biller whose status is pending_registration. The biller record is created in the system at the point of submission so that the back-office can track progress, but the biller code is not yet assigned at that stage.

CRN format configuration

Each biller defines the format rules used to validate the customer reference number presented on inbound payments:

Validation type Description
luhn CRN passes a Luhn check digit algorithm — the standard BPAY validation method
regex CRN is validated against a biller-specific regular expression (e.g. 8–12 digits with a specific prefix)
none No CRN validation applied — any non-empty string is accepted

The validation type and any format parameters are stored in crn_format and crn_validation_type. CRNs failing validation on receipt are quarantined (see FR-606).

Inbound payment processing

The sponsor bank delivers a daily settlement file containing all inbound BPAY payments received on behalf of this bank's registered billers. The processing flow is:

  1. File ingestion — the settlement file is received from the sponsor bank (SFTP or API, depending on sponsor bank capability) and each line item is written to payments.bpay_inbound_payments with status received.
  2. CRN validation — each payment's CRN is validated against the registered biller's CRN format. Items that fail validation are set to returned and reported to the operations queue.
  3. Credit posting — valid items are posted as credits to the biller's nominated account via MOD-001. The BPAY payment date (as indicated in the settlement file) and scheme reference are recorded on the ledger transaction.
  4. Notification — a payment received notification is dispatched to the biller via MOD-063 within 60 seconds of the posting completing.
  5. Reconciliation — the complete batch is reconciled against the sponsor bank's BPAY settlement file via MOD-081 (see FR-608).

Same-day crediting applies to all payments included in the sponsor bank's daily settlement file.

Data model

-- Registered BPAY billers
CREATE TABLE payments.bpay_billers (
    biller_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    account_id             UUID NOT NULL REFERENCES core.accounts(account_id),
    biller_code            TEXT UNIQUE,          -- null until sponsor bank assigns code
    biller_name            TEXT NOT NULL,
    crn_format             TEXT,                 -- regex or length spec; null for luhn/none
    crn_validation_type    TEXT NOT NULL DEFAULT 'luhn' CHECK (crn_validation_type IN (
                               'luhn', 'regex', 'none'
                           )),
    status                 TEXT NOT NULL DEFAULT 'pending_registration' CHECK (status IN (
                               'pending_registration',
                               'active',
                               'suspended',
                               'deregistered'
                           )),
    registered_at          TIMESTAMPTZ,          -- null until active
    scheme_reference       TEXT,                 -- BPAY Pty Ltd registration reference
    created_at             TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Inbound BPAY payments received via the sponsor bank settlement file
CREATE TABLE payments.bpay_inbound_payments (
    inbound_id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    biller_id              UUID NOT NULL REFERENCES payments.bpay_billers(biller_id),
    crn                    TEXT NOT NULL,
    payer_bank_code        TEXT,                 -- originating bank identifier from settlement file
    amount                 NUMERIC(18,2) NOT NULL,
    payment_date           DATE NOT NULL,        -- value date from settlement file
    settlement_batch_reference TEXT NOT NULL,
    posting_id             UUID,                 -- reference to MOD-001 ledger entry
    status                 TEXT NOT NULL DEFAULT 'received' CHECK (status IN (
                               'received',
                               'posted',
                               'reconciled',
                               'returned'
                           )),
    created_at             TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX ON payments.bpay_inbound_payments (biller_id);
CREATE INDEX ON payments.bpay_inbound_payments (settlement_batch_reference);
CREATE INDEX ON payments.bpay_inbound_payments (payment_date);

biller_code is nullable at creation — it is populated only once the sponsor bank returns the assigned code and the biller moves to active. scheme_reference is the BPAY Pty Ltd registration confirmation reference returned by the sponsor bank.

Key operations

Biller registration. Back-office creates a payments.bpay_billers record with status pending_registration and submits the registration request to the sponsor bank. When the sponsor bank confirms the assigned biller code, the record is updated to active with the biller code and registered_at timestamp.

CRN validation on receipt. For each item in the settlement file, the CRN is checked against the biller's crn_validation_type. Luhn validation applies the standard Luhn algorithm. Regex validation applies the pattern in crn_format. Items failing either check are set to returned; the payer's bank is notified via the BPAY scheme return mechanism, and the item is added to the operations exception queue.

Inbound credit posting. Valid items are posted via MOD-001 as credits to the biller's nominated account. The posting_id is written back to bpay_inbound_payments and status advances to posted.

Settlement reconciliation. After all items in a settlement batch are processed, MOD-081 reconciles the processed totals against the sponsor bank's BPAY settlement file — comparing item count and total credit amount per biller. Discrepancies are flagged to the operations queue and status remains posted rather than advancing to reconciled until the discrepancy is resolved.

Return handling. CRN validation failures result in a scheme return to the payer's bank via the sponsor bank. The return is recorded in bpay_inbound_payments with status returned. Returns that cannot be matched to a biller (e.g. unrecognised biller code) are escalated to the operations queue immediately.

Requirements

FR-605 — System shall manage the BPAY biller registration lifecycle — from back-office initiation of a new biller registration request (capturing biller name, account, CRN format) through to sponsor bank scheme registration confirmation; the biller record must remain in pending_registration status until the sponsor bank confirms the biller code has been assigned; no inbound payments may be credited to a biller in pending_registration status.

FR-606 — System shall validate the CRN on each inbound BPAY payment against the registered biller's CRN format specification (Luhn check, regex, or length-only as configured); CRNs that fail validation must be quarantined with status returned and reported to the operations queue within 60 minutes; the payer's bank must be notified via the scheme return mechanism.

FR-607 — System shall post each valid inbound BPAY receipt as a credit to the registered biller's account via MOD-001 on the day of receipt (same-day settlement for payments included in the sponsor bank's daily settlement file), record the BPAY payment date and scheme references on the transaction, and dispatch a payment received notification to the biller via MOD-063 within 60 seconds of posting.

FR-608 — System shall reconcile each inbound BPAY settlement batch against the sponsor bank's BPAY settlement file via MOD-081, identifying any discrepancy between expected and received totals per biller, and must flag unmatched items to the operations queue within 2 hours of the settlement file being received; unreconciled items must not remain open beyond end of the following business day without an escalation.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy AUTO Inbound BPAY payments are credited to the biller's account automatically upon receipt from the sponsor bank settlement file; no manual processing step for standard inbound payments.
REP-005 — Data Quality & Assurance Policy LOG All BPAY biller registrations, inbound payment receipts, and reconciliation outcomes are logged for payment data quality and regulatory reporting.
PAY-002 — Settlement Risk Policy LOG Biller registration events and inbound payment transactions are logged to the payment audit trail.
CON-005 — Fee & Pricing Transparency Policy AUTO Biller is notified of each inbound BPAY receipt within 60 seconds via MOD-063 — timely disclosure of payment received (j-1 ruling; CON-001 is a stretch fit for this mechanism).

MOD-137 — Agency banking adapter

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

Agency banking allows customers to perform basic cash transactions — deposits, withdrawals, and balance enquiries — at a third-party service point rather than at a branch or ATM. MOD-137 provides the adapter that ingests the daily transaction batch file delivered by the agency network, validates each item, posts it to the customer's account, and reconciles the batch against the agency network's settlement file.

Scope

Australia — primary integration is with Australia Post's agency banking service. Australia Post operates an extensive outlet network and is the dominant agency banking channel for Australian financial institutions. Integration is file-based: Australia Post delivers an encrypted SFTP file each business day containing all transactions processed across the agency network for this institution's customers.

New Zealand — configurable. NZ Post historically provided agency banking for NZ financial institutions but usage is significantly lower. The module supports NZ Post as an alternative agency network via the same file-based adapter; the agency_network field on the batch record determines which network's file format and settlement process applies. Additional agency network adapters (beyond Australia Post and NZ Post) can be configured via the other network type.

Agency banking is particularly important for customers in regional areas with limited branch coverage and for older demographic segments who prefer in-person cash transactions. It complements the digital channel rather than substituting for it.

Agency network integration model

Integration is file-based, not real-time. The agency terminal at the outlet does not communicate directly with the bank's platform during the transaction. The teller or terminal accepts the transaction, and the agency network collects all transactions across its outlets during the business day. At end of day, the network delivers a single encrypted batch file to the bank via SFTP.

Consequences of this model:

  • The customer's account balance is not updated at the point of the in-outlet transaction. Balance updates occur when the batch file is processed, typically within a few hours of end of business day.
  • A customer presenting at a second outlet on the same day may see a balance that does not reflect an earlier same-day agency transaction.
  • Withdrawal validation uses the end-of-prior-business-day available balance at the time of the agency terminal transaction; overdrafts that result from same-day intraday movements before the batch is processed are handled via the standard overdraft reconciliation process.

File format

The Australia Post agency banking file is a standard delimited format delivered as an encrypted file via SFTP. Each record contains:

Field Description
transaction_type deposit, withdrawal, or balance_enquiry
account_number Customer account number as entered at the terminal
bsb BSB of the nominated account
amount Transaction amount in AUD (zero for balance enquiries)
timestamp Date and time of the transaction at the outlet terminal
terminal_id Unique identifier of the outlet terminal
agent_outlet_code Australia Post outlet code

NZ Post file format follows the equivalent NZ agency banking specification. The agency_network value on the batch record drives format selection at parse time.

Data model

-- Agency batch files received from the network
CREATE TABLE payments.agency_batch_files (
    batch_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,   -- network-assigned file identifier
    transaction_date     DATE NOT NULL,
    item_count           INT NOT NULL,
    total_deposits       NUMERIC(18,2) NOT NULL DEFAULT 0,
    total_withdrawals    NUMERIC(18,2) NOT NULL DEFAULT 0,
    status               TEXT NOT NULL DEFAULT 'received' CHECK (status IN (
                             'received',
                             'processing',
                             'settled',
                             'reconciled',
                             'exceptions'
                         )),
    received_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Individual transactions from agency batch files
CREATE TABLE payments.agency_transactions (
    agency_tx_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    batch_id             UUID NOT NULL REFERENCES payments.agency_batch_files(batch_id),
    account_id           UUID REFERENCES core.accounts(account_id),  -- null if unmatched
    account_number_raw   TEXT NOT NULL,          -- as received in file, before matching
    transaction_type     TEXT NOT NULL CHECK (transaction_type IN (
                             'deposit', 'withdrawal', 'balance_enquiry'
                         )),
    amount               NUMERIC(18,2) NOT NULL DEFAULT 0,
    terminal_id          TEXT NOT NULL,
    agent_outlet_code    TEXT NOT NULL,
    status               TEXT NOT NULL DEFAULT 'matched' CHECK (status IN (
                             'matched',
                             'posted',
                             'quarantined',
                             'unmatched'
                         )),
    posting_id           UUID,                   -- reference to MOD-001 ledger entry
    failure_reason       TEXT,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX ON payments.agency_transactions (batch_id);
CREATE INDEX ON payments.agency_transactions (account_id);
CREATE INDEX ON payments.agency_transactions (status);

account_id is nullable: it is populated after account matching succeeds. Items that cannot be matched to an account are set to unmatched and escalated to the operations queue. terminal_id and agent_outlet_code are carried through to the ledger transaction as location metadata; these fields are required for AML cash transaction reporting (see FR-611).

Key operations

File receipt and validation. The batch file is received via SFTP, decrypted, and parsed. Basic structural validation (record count, file totals, format compliance) is run before individual items are processed. A payments.agency_batch_files record is created with status received.

Account matching. Each transaction item is matched to a live account in the platform by account_number_raw and BSB. Successfully matched items have account_id populated and status set to matched. Items that cannot be matched are set to unmatched with a structured failure reason and added to the operations queue within 2 hours.

Validation gate. For each matched item: account status is checked (the account must be active); for withdrawals, available balance is checked via MOD-003. Items failing either check are quarantined with the failure reason (account_inactive, insufficient_funds, etc.) and reported to the operations queue.

AML threshold check. Items where transaction_type is deposit or withdrawal and amount is at or above the jurisdiction threshold (AUD 10,000 / NZD 10,000) are flagged for cash transaction reporting via the AML monitoring platform. The flag is set before posting; it does not delay or block the posting (see FR-611).

Posting. Valid items are posted via MOD-001: deposits as credits, withdrawals as debits. The posting_id is written to agency_transactions and status advances to posted. Balance enquiry items do not result in a posting and are marked posted after a response is logged.

Customer notification. For each item posted to an account, a transaction notification is dispatched via MOD-063 within 60 minutes of the posting completing. The notification includes the transaction type, amount, and agency outlet reference.

Settlement reconciliation. After the batch is fully processed, MOD-081 reconciles the processed item count and totals against the agency network's settlement file. Discrepancies are flagged to the operations queue within 2 hours. Unresolved discrepancies are escalated to the agency network within 1 business day (see FR-612).

Exception handling. Quarantined and unmatched items are surfaced in the operations queue with the failure reason, the raw account number, and the outlet reference. The operations team can manually match unmatched items, override quarantine with justification, or initiate a return to the agency network. All manual interventions are logged.

Requirements

FR-609 — System shall process incoming agency batch files by parsing each transaction, matching the account number to a live account in the platform, and validating the account status and available balance (for withdrawals) before posting; items that cannot be matched to an account, or where the account fails validation, must be quarantined with a structured failure reason and reported to the operations queue within 2 hours of the batch being received.

FR-610 — System shall post each valid agency deposit as a credit and each valid agency withdrawal as a debit via MOD-001 on the transaction date indicated in the batch file, and must dispatch a transaction notification to the customer via MOD-063 within 60 minutes of the posting for each agency transaction.

FR-611 — System shall detect agency cash transactions at or above the jurisdiction-specific AML reporting threshold in the batch file and must flag these items for cash transaction reporting via the AML monitoring platform, recording the terminal_id and agent_outlet_code as location metadata on the transaction — the AML flag must not delay the posting of the transaction.

FR-612 — System shall reconcile each processed agency batch against the agency network's settlement file via MOD-081, comparing item counts and total amounts per transaction type; any discrepancy must be flagged to the operations queue within 2 hours of reconciliation completing; unresolved discrepancies must be escalated to the agency network within 1 business day.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE Each agency transaction in the batch file is validated against account status and available balance before posting; items that fail validation are quarantined with a structured failure reason.
AML-005 — Transaction Monitoring Policy AUTO Agency cash transactions at or above the AML reporting threshold (AUD/NZD 10,000) are flagged for cash transaction reporting; the agency batch file includes transaction amounts enabling threshold detection.
CON-001 — Customer Fairness & Conduct Policy AUTO Agency transactions are reflected in the customer's account balance and transaction history with the same speed as digital channel transactions — no delayed posting for the agency channel.
PAY-002 — Settlement Risk Policy LOG All agency batch files, individual items, and reconciliation outcomes are logged to the payment audit trail.

MOD-141 — Intra-bank transfer engine

System: SD04 | Repo: bank-payments | Build status: Deployed | Deployed: Yes

Purpose

Detects when both the payer and payee hold accounts at the same institution and executes the transfer as a direct book transfer — a single atomic double-entry posting — rather than routing through an external payment rail. Intra-bank transfers are immediate, carry no settlement risk, incur no external rail fees, and are not subject to cut-off windows.

When intra-bank transfer applies

Before routing a payment to an external rail (NPP, Payments NZ RTC, or batch), the payments system resolves the destination account identifier. If the destination account number or identifier resolves to an account held within the same institution, the payment is routed to the intra-bank transfer engine rather than any external rail.

The routing decision is made transparently to the customer: the confirmation screen labels the transfer as an immediate transfer and shows both the debit from the source account and the credit to the destination account. Customers do not see references to NPP or Payments NZ for intra-bank payments because no external rail is involved.

Intra-bank routing applies to:

  • Transfers between a customer's own accounts at the same institution (savings to transaction, transaction to term deposit on maturity).
  • Transfers from one customer's account to another customer's account at the same institution (personal payment, SME director paying company account).
  • Internal platform operations: fee collection from a customer account to an institution fee income account, interest postings from institution funding accounts.
  • Batch payment items (via MOD-135): when a batch contains items where the destination resolves to an internal account, those items are routed to the intra-bank engine rather than the external batch rail.

Advantages over external rail

Property Intra-bank book transfer External payment rail
Settlement timing Immediate — same transaction Minutes (NPP) to next business day (batch)
Settlement risk None — single atomic posting Counterparty and settlement risk between dispatch and settlement
External fees None Per-transaction or volume fees payable to rail operator
Cut-off windows None — 24/7/365 Batch rails have cut-off times; some real-time rails have maintenance windows
Failure modes Database transaction failure — fully atomic External rejection, timeout, settlement failure

AML considerations

Intra-bank transfers are not exempt from AML transaction monitoring. Internal transfers between accounts at the same institution are a well-documented money laundering typology: a customer receives funds from an external source and immediately moves them between their own accounts to obscure origin before withdrawal or onward transfer (layering).

For this reason, every intra-bank transfer passes through the same transaction fraud scoring (MOD-023) and AML screening pipeline as an external payment. The intra-bank routing flag is visible in the transaction record for analytical purposes, but it does not alter the screening logic or thresholds applied.

Data model

-- payments.intra_bank_transfers
CREATE TABLE payments.intra_bank_transfers (
  transfer_id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  source_account_id       UUID NOT NULL,
  destination_account_id  UUID NOT NULL,
  amount                  NUMERIC(18,2) NOT NULL CHECK (amount > 0),
  currency                TEXT NOT NULL,
  reference               TEXT,
  initiated_by            UUID NOT NULL,  -- customer or staff member ID
  channel                 TEXT NOT NULL CHECK (channel IN ('app','api','back_office','batch')),
  posting_id              UUID,           -- set when MOD-001 posting succeeds
  fraud_score_result      TEXT,           -- result code from MOD-023
  status                  TEXT NOT NULL DEFAULT 'pending'
                          CHECK (status IN ('pending','posted','failed')),
  failure_reason          TEXT,
  created_at              TIMESTAMPTZ NOT NULL DEFAULT now()
);

The posting_id references the double-entry posting created in MOD-001. The atomicity guarantee is at the database transaction level: the payments.intra_bank_transfers row is updated to posted in the same database transaction that creates the MOD-001 posting. If either write fails, both are rolled back.

Key operations

Initiation and routing detection. The payments orchestrator resolves the destination account identifier. If the account exists in the platform's account registry, the payment is flagged as intra-bank and routed here. If resolution fails (account not found internally), the payment is routed to the appropriate external rail based on the destination identifier format.

Pre-payment validation. MOD-020 runs pre-payment validation on the source account: available balance check (via MOD-003), account status check (not frozen, not closed), and transaction limit check. If any check fails, the transfer is rejected before fraud scoring and posting.

AML and fraud screening. MOD-023 scores the transaction. A high-risk score may result in a hold pending manual review, consistent with the handling of external payments at the same risk level. The score result is recorded in fraud_score_result regardless of outcome.

Atomic double-entry posting. On passing all validation, a single database transaction calls MOD-001 to create the double-entry posting: debit source account, credit destination account. The intra-bank transfer record is updated to posted in the same transaction.

Confirmation and notification. The source account holder receives a debit notification and the destination account holder receives a credit notification via MOD-063 within 10 seconds of the transfer posting.

Integration with batch payments

MOD-135 (batch payment processing) inspects each item in a batch before dispatching to external rails. Items where the destination resolves to an internal account are extracted from the batch and submitted to the intra-bank transfer engine individually. The batch item is marked as settled via intra-bank, and the batch settlement summary distinguishes intra-bank items from external rail items for reconciliation purposes.

Requirements

FR-625 — Routing detection: the system must detect whether a payment destination resolves to an account within the same institution before routing; if it does, the payment must be routed to the intra-bank transfer engine rather than any external rail; this routing decision must be transparent to the customer.

FR-626 — Atomic double-entry execution: intra-bank transfers must be executed as a single atomic double-entry transaction in MOD-001; the transfer must either fully complete or fully fail — partial postings are technically impossible because both the debit and credit are written in the same database transaction.

FR-627 — Validation parity: the system must apply the same pre-payment validation (MOD-020) and transaction fraud scoring (MOD-023) to intra-bank transfers as to external payments; intra-bank status must not be used to bypass or reduce any validation check.

FR-628 — Performance: the system must complete an intra-bank transfer from initiation to both accounts reflecting the updated balance within 5 seconds under normal conditions; the destination account holder must receive a credit notification via MOD-063 within 10 seconds of the transfer posting; these targets apply 24/7/365 with no cut-off window.

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE Intra-bank transfers pass pre-payment validation — available balance, account status, and transaction limits — before posting; the transfer is not posted if the source account has insufficient funds.
AML-007 — Sanctions Screening Policy AUTO Intra-bank transfers pass through the same transaction screening as external payments; internal transfers are a known layering typology and screening is not bypassed for intra-bank routing.
PAY-002 — Settlement Risk Policy LOG All intra-bank transfers are logged to the payment audit trail with both account IDs, the amount, the initiating channel, and the fraud score result.
CON-005 — Fee & Pricing Transparency Policy AUTO The customer is shown both the debit and credit side of the transfer for confirmation before execution, making the immediate book-transfer nature transparent.

MOD-144 — Confirmation of payee — account name verification

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

Implement Confirmation of Payee (CoP) for non-PayID outbound BSB/account number payments in Australia, satisfying the AU Scam-Safe Accord commitment to verify payee account names before any outbound payment is confirmed. MOD-120 already implements CoP for PayID-identified Osko payments. This module extends name verification to the majority of domestic payment volume — payments routed by BSB and account number without a PayID handle — closing the compliance gap left by MOD-120's scope.

What it does

NPP CoP service integration

The module integrates with the NPP Confirmation of Payee service, which operates over ISO 20022 messaging through the NPP infrastructure. For each qualifying outbound payment, the module submits the destination BSB, destination account number, and the payee name entered by the customer. The service returns one of four results:

  • match — the name provided matches the registered account name.
  • close_match — the name is similar but not identical; the registered name is returned.
  • no_match — no name similarity; the registered name may or may not be returned depending on the receiving institution's disclosure policy.
  • unavailable — the CoP service could not be reached or the receiving institution does not participate.

Presentation logic

Result handling in the payment confirmation flow:

  • match — a green match indicator is displayed. The customer may proceed normally without any additional acknowledgement step.
  • close_match — an amber warning is displayed alongside the actual registered name returned by the service. The customer must actively confirm they wish to continue before the payment instruction is accepted.
  • no_match — a red warning is displayed, including the registered name where the service has returned one. The customer must actively override, selecting a stated reason from a provided list (e.g. "Business trading name", "Nickname", "I have confirmed the name with the payee directly"). Free-text elaboration is optional but the reason code is required.
  • unavailable — an amber advisory message is displayed noting that name verification could not be completed. The customer may proceed; no override reason is required. The unavailable result is recorded against the payment.

Override capture and liability record

When a customer proceeds past a close_match or no_match result, the override reason is captured and stored. This record constitutes the Scam-Safe Accord liability documentation. Where the customer later claims they were deceived into making the payment, the existence and content of the override record is material to the reimbursement determination under the Accord's consumer reimbursement framework.

CoP checks table

All checks are recorded in payments.cop_checks:

Column Description
check_id UUID primary key
payment_id Foreign key to the payment instruction
destination_bsb BSB submitted to the CoP service
destination_account Account number submitted
name_provided Payee name as entered by the customer
result match / close_match / no_match / unavailable
registered_name Name returned by service (null if not returned)
override_reason Reason code selected by customer (null unless overridden)
acknowledged_at Timestamp of customer confirmation or override
checked_at Timestamp of CoP service call

Payment gate enforcement

The CoP check is enforced as a gate in the MOD-024 payment initiation flow. A payment instruction cannot be passed to the settlement layer without a cop_check_id reference. The gate is implemented at the payment service level, not in the UI layer, so it cannot be bypassed through alternative API entry points.

Scope and feature flag

CoP applies to AU-jurisdiction outbound BSB/account number payments only. The feature flag payments.cop.enabled controls activation at deployment time. NZ A2A payments use a different interbank infrastructure and are not subject to NPP CoP; this module has no effect on NZ payment flows.

Result caching

CoP results are cached for 60 seconds keyed on BSB and account number combination. If the customer amends the payment amount or reference and re-triggers the confirmation flow within the cache window, the cached result is used without re-querying the service. Cache entries are invalidated after 60 seconds. Name changes always trigger a fresh service call regardless of cache state.

Batch payments

For batch payment files processed by MOD-135, CoP is applied per item at file parse time before the batch is submitted to settlement. Items that return no_match are quarantined and surfaced for operator review in a dedicated exception queue. The operator may approve individual items (with reason recorded) or reject them. Items returning match, close_match, or unavailable pass through automatically, with close_match flagged in the batch report. The batch is not held waiting for the operator to review no_match items — qualifying items are submitted while quarantined items pend separately.

Scheduled and future-dated payments

MOD-025 scheduled and future-dated payments trigger a CoP check at initiation time. There is no re-check at execution time. If the customer's payee details change between initiation and execution, the original check result remains on record. This is consistent with industry practice under the Accord.

Compliance reason

The AU Scam-Safe Accord, signed by all participating ADIs including COBA credit union members, commits signatories to implement CoP across all outbound payment types. MOD-120 satisfies this obligation for PayID-identified payments. Outbound BSB/account number payments — representing the majority of domestic payment volume by count and value — are not covered by MOD-120. Without this module, the deploying institution's CoP implementation has a material gap and the institution is non-compliant with the Accord. The Accord sets compliance timelines enforced by ABA and reviewed by ASIC and the ACCC; non-compliance carries reputational and regulatory risk.

Commercial reason

CoP directly reduces authorised push payment (APP) fraud losses. Under the Accord's consumer reimbursement framework, a financial institution that has implemented CoP and documented a customer's informed override of a no_match result has a stronger position when assessing reimbursement liability than one that performed no name check at all. Reduced fraud losses flow through to lower provisioning requirements and lower operational cost from fraud investigation and recovery work. CoP also reduces false-routing errors — payments sent to a legitimate but unintended account due to a transcription mistake in the BSB or account number — which generate material operational cost in reversal requests, recalls through NPP, and customer remediation.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy GATE Payee name is verified against the destination account before the customer can confirm any outbound payment — name mismatch or no-match result is shown and must be acknowledged before proceeding.
PAY-003 — Card Scheme Compliance Policy AUTO CoP result and customer acknowledgement are recorded with the payment record for fraud liability and audit purposes.

MOD-145 — Payment hold & friction engine

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

Provide the ability to delay, hold, and subject to friction any outbound payment that exceeds a configurable risk threshold, satisfying the AU Scam-Safe Accord's obligation for participating banks to be capable of holding suspicious payments before they reach settlement. This is distinct from blocking a payment outright: the payment enters a held state, the customer is notified, and a defined resolution window applies before the payment is either released or cancelled.

What it does

Risk scoring integration. The payment rules engine (MOD-020) evaluates each outbound payment against a hold risk score before the payment is submitted to the settlement layer. Configurable signals that contribute to the score include: first payment to this payee, payment amount above a configurable threshold, payee added within the last N hours, a CoP no_match result from the confirmation of payee module (MOD-144), transaction velocity anomaly for the sending account, and destination account flagged by AML screening. The combined signal score is compared to a configurable hold threshold to determine whether and at what friction level the payment is held.

Three friction levels. The hold engine supports three levels of friction applied progressively based on risk score:

  • advisory — a warning is shown to the customer at the point of payment initiation. The customer may proceed freely; no queue delay is applied. Used for moderate-risk indicators where the institution wants to prompt reconsideration without creating friction.
  • soft_hold — the payment is queued and not submitted to settlement. The customer must actively reconfirm within a configurable hold window (default 24 hours) for the payment to proceed. If no action is taken before the window expires, the payment is auto-cancelled and the customer is notified. The customer may also cancel proactively at any point during the hold window.
  • hard_hold — the payment is queued and cannot be released by the customer. Back-office staff with the fraud_analyst role must review the payment and either release it to settlement or block it permanently. There is no expiry for hard_hold payments pending staff action; the institution's operational SLA governs review turnaround.

Payment holds table. The payments.payment_holds table records each hold event with the following columns: hold_id, payment_id, hold_level (advisory / soft_hold / hard_hold), hold_reason (structured reason codes corresponding to the signals that triggered the hold), risk_score, hold_at, hold_window_expires_at, resolved_at, resolution (customer_confirmed / customer_cancelled / staff_released / staff_blocked / expired_cancelled), and resolved_by.

Customer journey for soft_hold. When a payment is placed in soft_hold, the customer receives a push notification and sees an in-app banner linking to the held payment. The in-app view surfaces the payee details, the reason codes for the hold (in plain language), and two actions: reconfirm or cancel. If the customer reconfirms, the hold is lifted and the payment is submitted to settlement in the next processing cycle. If the customer cancels, the payment record is closed and funds are returned to the available balance immediately. If neither action is taken before hold_window_expires_at, the payment is auto-cancelled and a cancellation notification is sent.

Staff review queue. Hard_hold payments appear in the back-office fraud review queue accessible to users with the fraud_analyst role. Each queue item displays the full payment context (payee, amount, channel, payment history to this payee), the CoP result if available, the risk score and contributing signals, and the customer's recent contact history. Staff may release the payment to settlement or block it permanently. All staff decisions are logged with the resolved_by identity (staff user ID), timestamp, and optional notes. These records are immutable once written.

Hold window configuration. Hold window durations are configurable per deploying institution. The platform default is 24 hours for soft_hold and no expiry for hard_hold pending staff action. The RBNZ and Scam-Safe Accord do not mandate a specific window duration; the deploying institution sets the value appropriate to their risk management framework and operational capacity.

Bypass conditions. The hold engine is bypassed for intra-bank transfers processed through MOD-141, internal system-initiated payments, and BPAY payments to billers already recorded on the institution's trusted biller list. These payment types carry a different risk profile and are not subject to hold evaluation.

Operational metrics. Hold rate, release rate, customer cancellation rate, staff cancellation rate, and average hold duration are surfaced in the operational dashboard. These metrics allow the institution to monitor the effectiveness and calibration of the hold threshold over time.

Compliance reason

The AU Scam-Safe Accord requires signatory banks to hold suspicious outbound payments rather than processing them immediately at risk. Without a hold capability, the platform cannot satisfy this Accord commitment, and the deploying institution has no technical defence against rapid authorised push payment (APP) scams. By the time a customer reports an APP scam, the payment has already settled and the recipient has withdrawn the funds — recovery is negligible. The hold window is the point at which the institution can interrupt the fraud before it becomes irrecoverable. The hard_hold level provides an additional backstop for the highest-risk cases, where staff review before release is the appropriate control.

Commercial reason

Reduced APP fraud losses directly reduce reimbursement liability under the Scam-Safe Accord's consumer reimbursement framework, which places shared liability on the receiving and sending banks. Each prevented fraud event avoids both the direct reimbursement cost and the operational cost of fraud investigation, customer remediation, and dispute handling. The configurable friction system lets each deploying institution tune the hold threshold and friction levels to their risk appetite and customer experience tolerance without requiring a code change — the institution can tighten or loosen thresholds in configuration as their fraud data evolves.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy AUTO High-risk payments are automatically held pending customer reconfirmation or staff review — hold prevents immediate loss in the event of a scam or fraud attempt.
AML-007 — Sanctions Screening Policy ALERT Payments held for risk review generate an alert in the AML case management system for compliance assessment.

MOD-149 — Scam intelligence reporting & reimbursement

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

Satisfy the two Scam-Safe Accord obligations not covered by fraud detection or payment hold modules: (1) sharing scam intelligence with the ABA Scam Intelligence Hub, and (2) processing consumer reimbursement claims under the Accord's no-fault reimbursement framework. Together these complete the platform's full Scam-Safe Accord coverage alongside MOD-144 (Confirmation of Payee) and MOD-145 (payment hold).

What it does

Scam intelligence reporting

A weekly intelligence export is generated from confirmed scam events in the AML case management system (MOD-018) and payment fraud records. The export identifies scam typologies observed (investment scam, romance scam, impersonation, and others), mule account BSBs and account numbers where scam proceeds were received, payment corridors, and average loss amounts.

The export is formatted to the ABA Scam Intelligence Hub API specification and submitted automatically via secure API. Submission confirmation and hub reference numbers are stored against each export record.

The payments.scam_intelligence_submissions table records: submission_id, period_start, period_end, typologies_reported, mule_accounts_reported, total_cases, hub_reference, submitted_at, and status (submitted / acknowledged / failed).

The compliance team can suppress individual mule account records from a submission if legal proceedings are ongoing — a manual override flag prevents that record from being included in future exports until released.

This function is AU only. The ABA Scam Intelligence Hub is an Australian Banking Association initiative. The module is inactive for NZ-only deployments.

Consumer reimbursement

When a customer reports a scam payment, a reimbursement case is opened in MOD-053 as a scam_reimbursement case type. The case carries statutory SLA timers consistent with IDR obligations.

The reimbursement assessment workflow evaluates three factors: whether the bank's controls failed (CoP result from MOD-144, hold engine decision from MOD-145, fraud alerts raised), customer vulnerability indicators, and whether the customer took reasonable care. The Accord sets a no-fault default for cases where the bank's controls failed — in those cases, full reimbursement is the starting position unless exceptional circumstances apply.

The payments.scam_reimbursements table records: reimbursement_id, case_id, payment_id, claimed_amount, assessment_outcome (approved_full / approved_partial / declined), bank_fault_determination, customer_care_determination, approved_amount, reimbursement_gl_entry_id, decided_at, and decided_by.

Approved reimbursements are posted as a credit to the customer's account via a MOD-001 ledger entry. The offsetting debit posts to the institution's fraud loss GL account, which is configured in MOD-140. Declined reimbursements trigger the standard IDR communication process in MOD-053, with the customer notified of their right to escalate to AFCA.

Reimbursement metrics — case volumes, approval rates, average amounts, and time-to-decision — are reported monthly to the board risk committee and annually to the ABA as part of the Accord compliance report.

Compliance reason

The Scam-Safe Accord has two commitments addressed by this module. Intelligence sharing (Article 4 of the Accord) requires banks to contribute scam typology and mule account data to the shared intelligence platform — this is a collective defence obligation that cannot be met without a systematic export process. The consumer reimbursement framework (Article 5) requires participating institutions to assess and pay no-fault reimbursements in defined circumstances. Without a governed workflow and a ledger-connected reimbursement entry, cases are handled inconsistently and the institution cannot demonstrate Accord compliance. The IDR SLA timers ensure the reimbursement process also meets the existing dispute resolution obligations under RG 271.

Commercial reason

Scam intelligence sharing benefits the institution directly — intelligence contributed by other banks about mule accounts and typologies improves the platform's own fraud detection inputs. The reimbursement workflow, while representing a cost, replaces an ad hoc process that carries higher legal and reputational risk when handled inconsistently. A systematic assessment process also allows the institution to quantify and manage its reimbursement exposure over time, informing fraud loss provisioning and risk appetite decisions.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy LOG Scam typology reports and mule account intelligence are submitted to the ABA Scam Intelligence Hub on a defined schedule — intelligence sharing obligation met automatically.
CON-002 — Complaints & Internal Dispute Resolution Policy AUTO Scam reimbursement cases are tracked through the IDR workflow with statutory SLA timers — reimbursement decisions are documented and communicated within required timeframes.

MOD-154 — Correspondent banking risk gate

System: SD04 | Repo: bank-payments | Build status: Not started | Deployed: No

Purpose

The correspondent banking risk gate manages the full lifecycle of correspondent banking relationships — due diligence, onboarding approval, ongoing monitoring, and payment-level enforcement. Every outbound payment routed through a correspondent bank is checked against the approved correspondent registry; routing through an unapproved, suspended, or sanctioned correspondent is structurally impossible. The module also detects payable-through account patterns and nested correspondent relationships that exceed the institution's risk appetite.

What it does

Correspondent bank registry

All approved correspondent relationships are maintained in payments.correspondent_banks: correspondent_id, institution_name, BIC/SWIFT code, jurisdiction, AML_risk_rating (low/medium/high/prohibited), onboarding_date, last_review_date, next_review_due, approval_officers (list), approval_status (active/suspended/terminated), settlement_limit (maximum unsettled nostro exposure in AUD/NZD), and due_diligence_document_ids. Only correspondents with approval_status = active are eligible for payment routing.

Due diligence and onboarding workflow

A new correspondent relationship is initiated as a case in MOD-151 (Risk Case Console) with a structured due diligence checklist: AML programme assessment, beneficial ownership, FATF jurisdiction risk tier, sanctions history, regulatory standing in home jurisdiction, payable-through account usage, and disclosed nested correspondent relationships. Dual approval is required — Head of Payments and Chief Compliance Officer — before approval_status is set to active. The completed due diligence record is linked to the registry entry and retained for regulatory examination.

Payment routing gate

Before a payment is routed through a correspondent, the routing layer checks: (1) BIC is in the approved registry with status = active; (2) the correspondent's AML risk rating does not exceed the configured maximum for this payment type and corridor; (3) routing this payment would not cause the nostro settlement exposure to exceed the configured limit (from MOD-082); (4) the correspondent is not in a FATF non-cooperative jurisdiction above the permitted risk tier. A payment failing any check is blocked, an alert is sent to payments operations, and the block is logged in MOD-048.

Payable-through account detection

Payments arriving for further routing where the originating customer cannot be identified from the payment message are flagged and held for compliance review. The module checks for indicators in SWIFT MT/MX fields that suggest the correspondent is acting as a pass-through for an undisclosed originator.

Nested correspondent monitoring

The module tracks each approved correspondent's disclosed downstream correspondents. A routing chain that would pass through a prohibited or high-risk correspondent (nested) is blocked regardless of the immediate correspondent's approval status.

Compliance reason

FATF Recommendation 13 and its interpretive note impose specific due diligence requirements for correspondent banking: respondent institution AML programme assessment, enhanced due diligence for higher-risk respondents, prohibition on shell bank relationships, and payable-through account controls. The NZ AML/CFT Act 2009 s.22C and AU AML/CTF Act 2006 s.96 incorporate these FATF obligations. With PRD-004 (cross-border wallet) in the Tier 1 product set, these obligations apply at launch.

Commercial reason

A single payment routed through an unapproved correspondent is a regulatory breach that can result in enforcement action, de-risking by correspondent banks, and significant reputational damage. The onboarding gate and payment-level check make that outcome structurally impossible without requiring any manual intervention in the normal payment flow.

Policies satisfied:

Policy Mode Description
AML-009 — Correspondent Banking & Payments Policy GATE No payment may be routed through a correspondent bank that has not completed due diligence and received an active approval in the correspondent registry — enforced at the payment routing layer.
AML-007 — Sanctions Screening Policy GATE Every correspondent institution and named intermediary in a payment chain is screened against sanctions lists before routing — a sanctions hit blocks the payment regardless of prior approval.
PAY-002 — Settlement Risk Policy GATE Outbound payments are blocked when routing would cause the unsettled nostro exposure to a correspondent to exceed the configured settlement limit.
AML-008 — Cross-Border Transfer Reporting Policy LOG All correspondent-routed cross-border payments are flagged for IFTI/CMIR evaluation in the cross-border reporting pipeline.

SD05 — Credit Decisioning & Loan Platform

Repo: bank-credit | Business domain: BD05 | Tech owner: Credit Engineering | Build status: Not started

End-to-end credit lifecycle — real-time credit decisioning, loan origination, servicing, collections, and IFRS 9 provisioning.

Modules

ID Name Status ADR
MOD-027 Affordability calculator Not started ADR-014
MOD-028 Credit score & risk rating Not started ADR-014
MOD-029 Pre-approval engine Not started ADR-018
MOD-030 Stage allocation model Not started
MOD-031 ECL calculation & GL posting Not started

For full module specifications and acceptance criteria, see module specifications.

Critical constraints

  1. MOD-027 and MOD-028 must run on write-back values from Snowflake (SD06) — never call Snowflake inline during a customer request.
  2. All credit decisions must satisfy responsible lending obligations under CCCFA (NZ) and NCC (AU) before any credit is extended.
  3. MOD-031 ECL provisioning must complete by month-end close deadline (NFR-006).

Modules in SD05


MOD-027 — Affordability calculator

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Computes net disposable income from verified income, committed expenses, HEM/Henderson benchmarks, and existing debt. Documents calculation for every application. See ADR-014.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy CALC Responsible lending obligation met — affordability assessment documented automatically for every application
CRE-003 — Credit Decisioning & Scorecard Policy LOG Affordability calculation is the credit decision artefact — consistent, auditable, regulator-ready
CON-004 — Product Disclosure & Sales Practice Policy LOG Repayment amount, total interest payable, and total cost are captured on every affordability assessment row and returned in the response — the calling module (MOD-029) is responsible for invoking MOD-050 to deliver disclosure to the applicant before acceptance.
REP-005 — Data Quality & Assurance Policy LOG Credit decision data lineage from input to output fully traceable in Snowflake

MOD-028 — Credit score & risk rating

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Bureau score, internal behavioural score, and debt-to-income ratio combined into a single risk rating. Rating drives pricing, LVR limits, and provisioning parameters.

Policies satisfied:

Policy Mode Description
CRE-001 — Credit Risk Management Policy AUTO Credit risk rating applied consistently to every borrower — no subjective override without documented justification
CRE-003 — Credit Decisioning & Scorecard Policy LOG Scorecard governance — model version logged against every decision
DT-005 — Model Risk Management Policy LOG Credit model in model inventory, validated quarterly, performance monitored in Snowflake
CLQ-001 — Capital Adequacy Policy CALC Risk rating maps to Basel risk weight — feeds RWA calculation automatically

MOD-029 — Pre-approval engine

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Runs affordability and credit scoring on existing customers nightly. Produces pre-approved offer stored in Postgres. Customer acceptance is one-tap — no new assessment required. See ADR-018.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy AUTO Handler reads credit.affordability_assessments for the application and rejects with decision_type=DECLINE when the latest result is FAIL or absent — no code path approves a credit application without a passing affordability assessment; enforced by source-level scan and negative integration test.
CON-004 — Product Disclosure & Sales Practice Policy GATE POST /credit/applications/{id}/accept requires a disclosure_acknowledgement_id referencing a credit.disclosure_acknowledgements row recording the exact offer terms shown (content_hash covers rate, term, total interest, total cost) — returns 403 DISCLOSURE_NOT_ACKNOWLEDGED without it; no acceptance path bypasses this gate.
CRE-003 — Credit Decisioning & Scorecard Policy LOG Every credit.credit_decisions row carries affordability_assessment_id FK, credit_score, risk_rating, model_version (via score reference), and policy_refs — enforced by ADR-048 Cat 1 immutability trigger; structural test confirms non-null model_version and affordability_assessment_id on every persisted decision.

MOD-030 — Stage allocation model

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Assigns IFRS 9 stage (1/2/3) to each loan based on days past due, watchlist status, and SICR criteria. Stage changes are event-driven.

Policies satisfied:

Policy Mode Description
CRE-006 — Impairment & Provisioning Policy AUTO Staging criteria applied consistently to all loans — no subjective stage assignment
REP-004 — Financial Statements Policy AUTO IFRS 9 provision movements posted to GL automatically — no manual journal for provisioning
CLQ-001 — Capital Adequacy Policy CALC Stage 3 exposure feeds credit risk capital calculation — fully automated link

MOD-031 — ECL calculation & GL posting

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

PD × LGD × EAD computed at loan level. Collective provision for Stage 1/2, individual for Stage 3. GL entries posted daily.

Policies satisfied:

Policy Mode Description
CRE-006 — Impairment & Provisioning Policy CALC ECL calculated at loan level daily — no quarterly manual assessment
REP-004 — Financial Statements Policy AUTO Provision entries in GL sourced from validated model — no manual provision entries
DT-005 — Model Risk Management Policy LOG ECL model in model inventory — backtested and validated against actual losses

MOD-059 — Credit bureau submission engine

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Automate credit information submissions to NZ and AU licensed credit reporting bodies and manage the dispute resolution workflow, ensuring accurate and timely reporting under the NZ Credit Reporting Privacy Code and AU Comprehensive Credit Reporting regime.

What it does

The module draws credit account data from the governed credit data pipeline and prepares submission files in the formats required by each credit reporting bureau. Submissions include all mandatory CCR fields: account open date, credit limit, repayment history, and account closure or default information.

Submissions are scheduled to match each bureau's update frequency. Pre-submission validation checks completeness and applies bureau-specific business rules. Failed validations are quarantined and the responsible officer is alerted.

The module manages inbound credit information disputes from customers. Disputes are logged, the relevant bureau is notified, and the investigation workflow is tracked to resolution within the required timeframe. Corrected submissions are generated and submitted automatically where the dispute is upheld.

The module provides reporting on submission accuracy rates and dispute volumes for the CCO's quarterly Board report.

Compliance reason

REP-010 requires timely and accurate bureau submissions and a governed dispute resolution process. Manual bureau submissions create accuracy risks and lack the dispute tracking capability required by the credit reporting rules.

Commercial reason

Accurate credit bureau data supports the platform's own credit decisioning through cleaner bureau inputs. It also protects against regulatory findings and customer complaints arising from inaccurate credit reporting.

Policies satisfied:

Policy Mode Description
REP-010 — Credit reporting & bureau submission AUTO Automates credit information submissions to NZ and AU bureaus and manages the dispute resolution workflow.

MOD-065 — Credit servicing & collections

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Credit servicing and collections covers the full post-origination lifecycle of a loan — from first repayment through to full settlement or, in adverse cases, default and write-off. This module fills the gap between loan approval (handled by MOD-029) and impairment provisioning (handled by MOD-031), managing the customer relationship and operational processes in between.

For customers in good standing, the module provides a repayment schedule view, payment date change capability, early repayment and settlement requests, and accurate running balance at any point in the loan term. For customers in arrears, it runs the collections workflow: automated reminders at configured intervals, escalation to the collections team, and hardship assessment when triggered.

The hardship flow is designed to meet regulatory obligations under the CCCFA (NZ) and NCCP (AU): customers can request relief through the app, the module evaluates eligibility against configured criteria, and approved restructures are journalled atomically so the loan balance and repayment schedule remain consistent with the ECL inputs in MOD-031.

v2 scope: hardship fee-waiver check endpoint

MOD-065 v2 will expose a synchronous fee-waiver check endpoint:

GET /hardship/fee-waiver-check?party_id=&fee_kind=
→ { waived: boolean, reason: string | null, arrangement_id: uuid | null }

This endpoint answers whether an active hardship arrangement covers a specific fee type for a given party. It will replace the v1 tactical bridge used by MOD-117 (overdraft) and any other fee-engine callers that currently read MOD-007's hardship flag directly.

v1 bridge (in force now): MOD-117 reads MOD-007's hardship-flag-read-url and waives all overdraft fees when the flag is set. This is the correct v1 behaviour — flagged = full waiver — because the v1 hardship arrangement does not have per-fee-kind terms. MOD-065 v2 adds the per-fee-kind granularity when product requirements call for it.

Trigger for v2: When a credit product is launched with a hardship arrangement that specifies fee-kind-level waiver conditions (e.g. "waive maintenance fees but not late payment fees"), the v2 endpoint becomes necessary. Until then, MOD-117's direct MOD-007 flag read is correct and sufficient.

Policies satisfied:

Policy Mode Description
CRE-006 — Impairment & Provisioning Policy AUTO Tracks loan stage transitions (current → arrears → default) and triggers the impairment events that feed the ECL engine.
CON-003 — Vulnerable Customer Policy AUTO Routes customers who meet hardship criteria into the hardship assessment workflow with appropriate communications triggered.
CRE-002 — Responsible Lending Policy ALERT Alerts the collections team when a customer misses a payment and escalates according to the collections policy timeline.

MOD-066 — Collateral & security management

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Collateral and security management is the operational register of assets pledged as security for credit exposures. It serves as the system of record for what assets back which loans, at what valuations, under what terms — providing the data that feeds the risk engine (MOD-030), the capital model (MOD-033), and the exposure calculations (MOD-084).

The module supports the full lifecycle of collateral: registration at origination (linking a property, vehicle, receivables pool, or personal guarantee to a facility), periodic revaluation (accepting updated appraisals and market values), monitoring (alerting when coverage falls below the loan-to-value covenant), and release (managing the discharge of security on repayment or substitution).

Customers with secured facilities can view their pledged assets, upload valuation reports, and track the release of security through the app. Credit staff use the back-office view to run the collateral register, process revaluations, and action coverage alerts. Required for SME lending, invoice finance, reverse mortgage, and any facility backed by real collateral.

Policies satisfied:

Policy Mode Description
CRE-005 — Concentration Risk Policy LOG Maintains the register of collateral supporting each exposure — used to calculate net exposure for concentration limit monitoring.
CRE-001 — Credit Risk Management Policy CALC Feeds current collateral coverage ratios into credit risk monitoring so secured and unsecured exposure is correctly tracked.

MOD-115 — Property security and LVR management

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Registers and monitors the property security instrument attached to each residential mortgage. Calculates current loan-to-value ratio (LVR) in real time using the outstanding loan balance (sourced from MOD-003) and the most recent property valuation on record. Enforces LVR policy gates at origination — a loan cannot proceed to settlement unless a registered security instrument is recorded and the calculated LVR falls within policy limits. Monitors ongoing LVR against policy thresholds and emits breach events for credit team review.

Compliance rationale

RBNZ BS19 requires the bank to measure, restrict, and report lending in defined LVR bands. Specifically, the bank must not exceed prescribed portfolio concentration limits in high-LVR categories (above 80% for owner-occupier; above 70% for investor). This module provides the per-loan LVR calculation and the daily LVR snapshot table that feeds those portfolio reports.

APRA APS 220 requires ongoing collateral adequacy monitoring for secured credit exposures. The daily LVR snapshot and valuation currency checks in this module satisfy that requirement.

CRE-005 (concentration risk policy) requires that LVR band distribution is tracked at portfolio level and reported to the credit risk committee. This module is the sole source of LVR band data for that report.

REP-002 (prudential reporting policy) requires that LVR band distribution is included in RBNZ and APRA prudential returns. This module's output feeds those returns directly.

Commercial rationale

LVR is the primary driver of mortgage pricing. LVR bands map to margin tiers in the rate sheet — a borrower at 70% LVR pays a materially lower rate than a borrower at 85% LVR. Accurate, real-time LVR data is therefore a direct revenue input.

Ongoing LVR monitoring creates two commercial opportunities. When LVR improves (property value rises or balance reduces), the bank can proactively offer a better rate tier — a retention and cross-sell trigger. When LVR deteriorates (property value falls), the bank can act early through a collateral call or rate repricing rather than discovering the problem at default.

Data model

-- credit.property_securities
CREATE TABLE credit.property_securities (
  security_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL REFERENCES credit.loans(loan_id),
  title_reference    TEXT NOT NULL,
  property_address   JSONB NOT NULL,  -- {street, suburb, city, postcode, country}
  property_type      TEXT NOT NULL CHECK (property_type IN ('residential','rural_residential','apartment','townhouse')),
  registered_at      TIMESTAMPTZ NOT NULL,
  registration_number TEXT,
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','discharged','suspended')),
  discharged_at      TIMESTAMPTZ,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.property_valuations
CREATE TABLE credit.property_valuations (
  valuation_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  security_id        UUID NOT NULL REFERENCES credit.property_securities(security_id),
  valuation_date     DATE NOT NULL,
  valuation_amount   NUMERIC(18,2) NOT NULL,
  valuation_type     TEXT NOT NULL CHECK (valuation_type IN ('full','desktop','avm','indexed')),
  valuer_reference   TEXT,
  source             TEXT NOT NULL,  -- 'registered_valuer','avm','index_update'
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.lvr_snapshots (materialised daily)
CREATE TABLE credit.lvr_snapshots (
  snapshot_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL,
  security_id        UUID NOT NULL,
  snapshot_date      DATE NOT NULL,
  outstanding_balance NUMERIC(18,2) NOT NULL,
  current_valuation  NUMERIC(18,2) NOT NULL,
  lvr_pct            NUMERIC(7,4) NOT NULL,  -- e.g. 0.7823 = 78.23%
  lvr_band           TEXT NOT NULL,  -- '<60','60-70','70-80','80-90','>90'
  policy_breach      BOOLEAN NOT NULL DEFAULT false,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (loan_id, snapshot_date)
);

Key operations

1. Security registration at settlement

Called by MOD-116 on drawdown. Inserts a record into credit.property_securities with the title reference, property address, property type, and registration details. Records the origination valuation in credit.property_valuations. Calculates the origination LVR and writes the first credit.lvr_snapshots record. If the calculated LVR exceeds the policy limit configured for the product, the settlement gate is blocked and an error is returned to the calling workflow.

2. LVR calculation

lvr = outstanding_balance / current_valuation

Run daily via a scheduled batch job (after MOD-003 end-of-day balance close) and on each valuation update. The result is written to credit.lvr_snapshots. The LVR band is derived from configurable thresholds: <60, 60-70, 70-80, 80-90, >90. Band boundaries are configurable to allow for regulatory threshold changes without a code deployment.

3. LVR breach detection

After each LVR calculation, the result is compared against the policy breach threshold for the product (configurable per product, per jurisdiction). If LVR exceeds the threshold, the policy_breach flag is set on the snapshot record and a bank.credit.lvr_breach_detected event is emitted. This event triggers an ALERT in MOD-063 (notification orchestration) to the credit team and flags the loan for review in the back-office queue.

4. AVM index update

MOD-085 provides quarterly property price index updates by suburb/region. On receipt of an index update, this module applies the index factor to all credit.property_valuations records with valuation_type = 'indexed' or where the most recent full valuation is older than the index refresh threshold (configurable, default 12 months). A new credit.property_valuations record is inserted with source = 'index_update' and valuation_type = 'indexed'. LVR snapshots are recalculated for all affected loans immediately after index update.

5. Security discharge

Called by MOD-116 on loan payoff or external refinance. Sets the status field on credit.property_securities to discharged and records discharged_at. Emits a bank.credit.security_discharged event. MOD-001 posts the security release accounting entry.

Requirements satisfied

FR-521 — System shall register a property security instrument against a loan at settlement and prevent drawdown if the security record is absent or LVR exceeds the policy gate threshold.

FR-522 — System shall calculate LVR for each active secured loan daily using end-of-day outstanding balance and the most recent valuation, and store the result in credit.lvr_snapshots.

FR-523 — System shall emit a bank.credit.lvr_breach_detected event and set policy_breach = true on the snapshot record when a loan's LVR exceeds the configured policy threshold.

FR-524 — System shall update property valuations on receipt of AVM index data from MOD-085 and recalculate LVR snapshots for all affected loans within the same batch run.

Policies satisfied:

Policy Mode Description
CRE-001 — Credit Risk Management Policy GATE Loan cannot settle without a registered security instrument recorded and LVR calculated within policy limits.
CRE-005 — Concentration Risk Policy CALC Current LVR is calculated daily and contributes to concentration risk reporting at portfolio level.
CLQ-002 — Liquidity Risk Management Policy CALC Secured loan balances and collateral values are included in liquidity stress calculations via this module's output.
REP-002 — Prudential Reporting Policy CALC LVR band distribution is reported in prudential returns (RBNZ BS19 / APRA APS 220).

MOD-116 — Mortgage servicing engine

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Manages the full post-drawdown lifecycle of a residential mortgage: scheduled repayment execution, fixed rate period management (including expiry notifications and rate elections), early repayment and break cost calculation, discharge processing, and arrears detection and escalation. This is the operational engine that runs a mortgage from settlement to payoff.

Compliance rationale

CCCFA (NZ) and NCCP (AU) require that customers receive adequate notice before their interest rate changes on a credit contract. ASIC RG 274 requires the bank to notify customers of significant product changes. This module enforces those obligations by driving the fixed rate expiry notification sequence at 90, 60, and 30 days before the expiry date.

Break cost disclosure before early repayment is mandatory under responsible lending obligations in both jurisdictions. A customer cannot submit an early repayment request through any channel until the break cost has been calculated, disclosed, and explicitly accepted.

Arrears early escalation to hardship is required under CON-008 and the industry banking codes in both NZ and AU. This module detects missed repayments at day 1 and drives a structured escalation sequence that routes accounts to the financial hardship workflow before any collections action is taken.

Commercial rationale

Fixed rate roll-off is one of the highest-value customer engagement moments in retail banking. A customer whose fixed rate expires without proactive contact will typically reprice with a competitor. The notification and election workflow in this module creates structured touchpoints at 90, 60, and 30 days that give the bank three opportunities to retain the customer before expiry.

Break cost transparency builds trust and reduces disputes. Customers who receive a clear, upfront break cost calculation before they refinance are significantly less likely to raise a complaint or seek external dispute resolution.

Fixed rate lifecycle

State machine: VARIABLEFIXEDEXPIRINGEXPIREDVARIABLE | FIXED (re-fixed)

VARIABLE: Default state for accounts on a variable rate, or accounts that have reverted after fixed period expiry.

FIXED: Entered on drawdown (fixed rate election) or when a customer successfully elects a new fixed term. The credit.mortgage_rate_periods record is created with rate_type = 'fixed', end_date set to the expiry date, and disclosed_at set when disclosure was delivered.

EXPIRING: Entered automatically when 90 days remain before end_date. The first expiry notification (expiry_90d) is dispatched via MOD-063. Subsequent notifications are sent at 60 days (expiry_60d) and 30 days (expiry_30d). The account remains in EXPIRING until the customer makes an election or the end date is reached.

EXPIRED: If no election is made by the end_date, the account reverts to the prevailing variable rate automatically. A expired_revert notification is sent. No penalty applies for automatic reversion. The credit.mortgage_rate_periods record for the fixed period is updated with status = 'expired' and a new variable rate period record is created.

Rate election: Customer (via app) or agent (via back office) elects variable rate or a new fixed term. MOD-050 enforces delivery and acknowledgement of the required disclosure before the election is accepted. On acceptance, a new credit.mortgage_rate_periods record is created with elected_at and disclosed_at set. The new rate takes effect on the current period's end_date.

Break cost calculation

Applies when a fixed rate loan is repaid in full or refinanced before the fixed term expires.

break_cost = max(0, (contract_rate − reinvestment_rate) × outstanding_balance × remaining_fixed_days / 365)

contract_rate is the rate on the active credit.mortgage_rate_periods record. reinvestment_rate is the wholesale swap rate for the remaining fixed term, sourced from MOD-085 at the time of calculation. remaining_fixed_days is the number of calendar days between the requested repayment date and end_date.

The break cost calculation result is stored in credit.break_cost_disclosures. The customer must acknowledge and accept the disclosed amount (accepted_at must be set) before the early repayment transaction is submitted to MOD-001 for posting. Break cost is posted as a separate ledger entry by MOD-001.

Data model

-- credit.mortgage_rate_periods
CREATE TABLE credit.mortgage_rate_periods (
  period_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL REFERENCES credit.loans(loan_id),
  rate_type          TEXT NOT NULL CHECK (rate_type IN ('variable','fixed')),
  rate_pct           NUMERIC(8,5) NOT NULL,
  start_date         DATE NOT NULL,
  end_date           DATE,  -- null for variable; set for fixed
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','expired','superseded')),
  elected_at         TIMESTAMPTZ,
  disclosed_at       TIMESTAMPTZ,  -- must be set before rate election accepted
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.mortgage_notifications
CREATE TABLE credit.mortgage_notifications (
  notification_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL,
  notification_type  TEXT NOT NULL CHECK (notification_type IN ('expiry_90d','expiry_60d','expiry_30d','expired_revert','rate_elected','break_cost_disclosure','discharge_initiated','arrears_day1','arrears_day7','arrears_day30')),
  sent_at            TIMESTAMPTZ,
  acknowledged_at    TIMESTAMPTZ,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.break_cost_disclosures
CREATE TABLE credit.break_cost_disclosures (
  disclosure_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL,
  disclosed_at       TIMESTAMPTZ NOT NULL,
  contract_rate      NUMERIC(8,5) NOT NULL,
  reinvestment_rate  NUMERIC(8,5) NOT NULL,
  outstanding_balance NUMERIC(18,2) NOT NULL,
  remaining_days     INT NOT NULL,
  break_cost_amount  NUMERIC(18,2) NOT NULL,
  accepted_at        TIMESTAMPTZ,  -- set when customer confirms
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

Arrears detection and escalation

At end of day, MOD-005 accrual output is compared against the scheduled repayment due for each active mortgage account. If a repayment is missed by end of business on the due date, the event bank.credit.repayment_missed is emitted and the escalation sequence begins:

Day 1: Notification of missed repayment dispatched via MOD-063 (arrears_day1). Record inserted into credit.mortgage_notifications.

Day 7: Hardship flag set on the account record in MOD-007 (customer profile). Back-office alert dispatched via MOD-063 (arrears_day7). Account is routed to the financial hardship queue for proactive outreach. No collections action is initiated until the hardship assessment is complete.

Day 30: Account escalated to MOD-065 (credit servicing and collections) for formal collections workflow. Notification dispatched (arrears_day30). All transitions are logged to credit.mortgage_notifications.

This sequence satisfies CON-008 by ensuring hardship routing precedes any collections action. The day thresholds are configurable per product to allow for code-free adjustment if regulatory guidance changes.

Requirements satisfied

FR-525 — System shall manage the fixed rate lifecycle state machine for each mortgage account, including automatic reversion to variable rate on expiry and dispatch of notifications at 90, 60, and 30 days before the fixed rate end date.

FR-526 — System shall calculate break cost using the formula max(0, (contract_rate − reinvestment_rate) × outstanding_balance × remaining_fixed_days / 365), store the result in credit.break_cost_disclosures, and enforce customer acceptance before posting the early repayment transaction.

FR-527 — System shall process loan discharge requests by calculating any applicable break cost, confirming zero arrears via MOD-065, posting the final repayment via MOD-001, and triggering security discharge in MOD-115.

FR-528 — System shall detect missed repayments at end of day and drive the arrears escalation sequence: notification at day 1, hardship flag at day 7, escalation to MOD-065 at day 30.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy AUTO Fixed rate expiry notification at 90/60/30 days ensures customers have adequate time to make an informed rate election before reversion to variable.
CON-004 — Product Disclosure & Sales Practice Policy AUTO Rate election disclosure is enforced before the customer's rate election is accepted — customer cannot elect without confirming they have received the disclosure.
CON-005 — Fee & Pricing Transparency Policy GATE Break cost calculation is disclosed to the customer before any early repayment of a fixed rate loan is processed.
CON-008 — Financial Hardship Policy ALERT Arrears escalation triggers a hardship flag, routing the account to the financial hardship workflow before collections action.
PAY-001 — Payment Operations Policy AUTO Scheduled repayments are initiated as automatic payments via the payment engine on their due date.

MOD-117 — Overdraft management engine

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Manages the full lifecycle of a revolving credit limit attached to a transaction account (PRD-019 Linked Transaction Overdraft). Responsibilities include: maintaining the approved limit record, tracking the drawn balance, calculating daily interest on any negative balance, posting monthly interest and facility fee charges, enforcing payment declines at the limit boundary, monitoring for financial hardship indicators, and handling the unarranged overdraft edge case.

MOD-117 operates as a sub-ledger on top of the core balance engine (MOD-003). It does not hold balances directly — all monetary postings flow through MOD-001 (double-entry posting engine). MOD-117 is the authoritative source for facility terms, accrual records, and the facility event log.


Compliance rationale

Under the NZ Credit Contracts and Consumer Finance Act 2003 (CCCFA) and the AU National Consumer Credit Protection Act 2009 (NCCP), a linked overdraft is a continuing credit contract. This creates several ongoing obligations that MOD-117 is designed to operationalise:

Responsible lending at origination and limit increase. The bank must perform a creditworthiness and affordability assessment before granting or increasing any limit. MOD-117 enforces this as a hard gate: no facility record can be created and no limit can be increased without a completed assessment reference from MOD-027 and MOD-028.

Persistent overdraft use as a hardship signal. Both CCCFA and NCCP require lenders to act proactively when a borrower shows signs of financial difficulty. A customer who is consistently at or near their overdraft limit for an extended period is exhibiting that signal. MOD-117 monitors consecutive days in a drawn state and emits a hardship flag at the 60-day threshold, triggering the proactive hardship conversation required by CON-008 and CRE-007.

Unarranged overdraft notification. The NZ and AU Banking Codes require prompt notification when a customer enters an unarranged overdraft (a negative balance where no overdraft facility has been granted). MOD-117 detects this condition and triggers an immediate customer notification via MOD-063.

Disclosure before activation. CON-005 requires that the limit, interest rate, and fee are disclosed before the facility is activated. MOD-117 will not create a facility record until MOD-050 confirms that the initial credit disclosure has been delivered and acknowledged.


Commercial rationale

Linked overdrafts are a high-margin retail credit product with low origination cost relative to other credit products: no security assessment, no title search, and minimal ongoing servicing overhead. The facility charges daily interest on the drawn balance, and daily interest income on even modest utilisation rates is material at scale.

The retention effect is significant. Customers with an overdraft attached to their transaction account have measurably lower churn than those without, because the overdraft increases the friction of switching to a competing bank (the customer would lose the pre-approved credit line and face a new application elsewhere). The overdraft is therefore both a revenue product and a retention mechanism for the core transaction account relationship.


Data model

-- credit.overdraft_facilities
CREATE TABLE credit.overdraft_facilities (
  facility_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id         UUID NOT NULL REFERENCES core.accounts(account_id),
  approved_limit     NUMERIC(18,2) NOT NULL,
  current_limit      NUMERIC(18,2) NOT NULL,  -- may differ if partial reduction pending
  interest_rate_pct  NUMERIC(8,5) NOT NULL,
  facility_fee       NUMERIC(18,2) NOT NULL,
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','suspended','closed')),
  review_date        DATE NOT NULL,
  activated_at       TIMESTAMPTZ NOT NULL,
  closed_at          TIMESTAMPTZ,
  last_assessment_id UUID,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.overdraft_daily_accruals
CREATE TABLE credit.overdraft_daily_accruals (
  accrual_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  facility_id        UUID NOT NULL REFERENCES credit.overdraft_facilities(facility_id),
  accrual_date       DATE NOT NULL,
  drawn_balance      NUMERIC(18,2) NOT NULL,
  daily_interest     NUMERIC(18,6) NOT NULL,
  posted             BOOLEAN NOT NULL DEFAULT false,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (facility_id, accrual_date)
);

-- credit.overdraft_events
CREATE TABLE credit.overdraft_events (
  event_id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  facility_id        UUID NOT NULL REFERENCES credit.overdraft_facilities(facility_id),
  event_type         TEXT NOT NULL CHECK (event_type IN ('limit_set','limit_increased','limit_reduced','limit_suspended','interest_charged','hardship_flag','utilisation_alert','review_due','closed')),
  event_data         JSONB,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

overdraft_facilities is the master record for each facility. current_limit may be lower than approved_limit during a partial reduction notice period. last_assessment_id links to the most recent affordability assessment record in the credit assessment service.

overdraft_daily_accruals stores one row per facility per day on which a negative balance was recorded. The posted flag distinguishes accruals that have been included in a monthly charge from those still pending. The unique constraint on (facility_id, accrual_date) prevents duplicate accrual runs.

overdraft_events is an append-only audit log of all significant lifecycle events on the facility. event_data carries structured JSON payload appropriate to each event type (e.g. old and new limit values for limit_reduced, consecutive days count for hardship_flag).


Key operations

1. Available balance

MOD-003 calls get_available_balance(account_id) when computing the balance available for a payment or display. MOD-117 returns ledger_balance + current_limit if an active facility exists for the account, or ledger_balance if no active facility exists. This combined figure is what MOD-021 (payment limit and velocity controller) uses to gate payment execution: a payment that would take the resulting balance below zero minus the limit is declined.

2. Daily interest accrual

A scheduled job runs at end of each calendar day. For each active facility where the associated account's ledger balance is negative:

daily_interest = abs(ledger_balance) × (interest_rate_pct / 100 / 365)

A row is inserted into overdraft_daily_accruals with posted = false. If the balance is zero or positive, no accrual row is created for that day. MOD-005 (daily accrual calculator) provides the end-of-day balance snapshot; MOD-117 owns the accrual record.

The consecutive-days-drawn counter is updated on each daily run. If the counter reaches 60, the hardship detection flow (see below) is triggered.

3. Monthly interest charge

On the last calendar day of each month, MOD-117 runs the monthly close job for all active facilities:

  1. Select all overdraft_daily_accruals rows for the facility where posted = false.
  2. Sum daily_interest across all selected rows.
  3. Post a single debit entry to the account via MOD-001 for the summed amount, with description identifying the period.
  4. Mark all selected accrual rows posted = true.
  5. Insert a interest_charged event into overdraft_events.
  6. Emit bank.credit.overdraft_interest_charged domain event.

4. Monthly facility fee

On the same monthly close run: check whether any overdraft_daily_accruals row exists for the facility in the calendar month (i.e. whether the balance was ever negative). If yes, post a facility fee debit via MOD-001 using the fee amount from overdraft_facilities.facility_fee. If no rows exist (balance was positive throughout the month), the fee is waived: a fee waiver event is logged in MOD-110 and an interest_charged event with zero fee amount is recorded for audit completeness.

5. Hardship detection

The daily accrual job tracks the number of consecutive calendar days on which a negative balance was recorded. When this count reaches 60:

  1. MOD-117 inserts a hardship_flag event into overdraft_events with the consecutive-days count in event_data.
  2. MOD-007 sets the hardship_review_pending flag on the account.
  3. MOD-063 dispatches an alert to the back-office operations queue and a customer-facing notification advising them to contact the bank if they are experiencing financial difficulty.
  4. The consecutive-days counter is not reset until the balance returns to positive and remains positive for at least 5 days, preventing repeated alerts on minor balance oscillations.

The hardship flag does not automatically restrict the account. It creates a task for a human review. The outcome of that review may result in a hardship arrangement under CON-008, at which point MOD-007 applies the appropriate account restrictions.

6. Unarranged overdraft

An unarranged overdraft occurs when an account with no active overdraft facility records a negative balance. This is an edge case that can arise from timing issues in payment settlement or fee debits.

On detection (MOD-007 observes a negative balance on an account with no active facility):

  1. MOD-007 sets the unarranged_overdraft state on the account.
  2. MOD-063 dispatches an immediate customer notification.
  3. MOD-117 creates a flag record for manual operations review.
  4. If the bank's product rules permit an unarranged overdraft fee (configured in MOD-110), the fee is posted via MOD-001.
  5. The account is not blocked from incoming credits. The balance is expected to return to positive when the next credit arrives.

If the balance remains negative beyond a configurable threshold period (default 5 business days), the operations queue receives an escalation alert.


FRs satisfied

FR Description
FR-529 System shall maintain an approved overdraft limit per transaction account and enforce that no payment or debit causes the balance to fall below the negative of that limit.
FR-530 System shall calculate available balance as ledger balance plus undrawn overdraft limit for all accounts with an active overdraft facility, and expose this figure to balance display and payment validation.
FR-531 System shall accrue overdraft interest daily on any negative ledger balance and post a single aggregated interest charge debit on the last calendar day of each month.
FR-532 System shall monitor consecutive days with a negative balance per facility and emit a financial hardship flag when the threshold of 60 consecutive days is reached.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy GATE Overdraft limit cannot be set or increased without a completed affordability assessment satisfying responsible lending obligations.
CON-005 — Fee & Pricing Transparency Policy GATE The current overdraft limit, interest rate, and monthly facility fee are disclosed to the customer before any limit is activated.
CON-008 — Financial Hardship Policy ALERT Customers who are drawn on their overdraft for more than 60 consecutive days are flagged for financial hardship review.
PAY-001 — Payment Operations Policy GATE Outgoing payments that would exceed the combined available balance (credit + overdraft limit) are declined before execution.
CRE-001 — Credit Risk Management Policy CALC Drawn overdraft balances contribute to credit exposure reporting and concentration risk calculations.

MOD-121 — Construction loan drawdown engine

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Manages the progressive drawdown lifecycle for construction loans. A construction loan disburses in tranches tied to defined construction milestones — slab, frame, lock-up, fixing, and completion — rather than as a single lump sum at settlement. Interest accrues only on the drawn balance. On completion, the loan converts to a standard residential mortgage with a full P&I amortisation schedule generated by MOD-112.

Compliance rationale

CCCFA (NZ) and NCCP (AU) responsible lending obligations extend to construction lending. CRE-002 requires that drawdowns are conditioned on verified construction progress — not on a calendar schedule alone. Releasing funds against an unverified milestone would expose the bank to a regulatory finding that it did not adequately monitor the use of credit. LVR monitoring under RBNZ BS19 (NZ) and APRA APS 112 (AU) must be updated after each drawdown: the loan balance increases with each tranche while the security value (an in-progress build) may lag or fluctuate, making per-drawdown LVR tracking an ongoing prudential requirement rather than a one-time assessment at origination.

Commercial rationale

Construction lending is a core product category for building societies and regional banks serving owner-builders and new build purchasers. Progressive drawdown protects both parties: the bank does not release funds until a qualifying certifier confirms the milestone is complete, and the customer is not charged interest on funds not yet drawn. Without this module, the platform can only offer land purchase loans or fully advanced loans, excluding the most common form of residential construction financing. The absence of construction lending capability would also constrain the bank's ability to serve first-home buyers using new build programmes (NZ First Home Loan, FHLDS in AU) which predominantly involve construction contracts.

Construction phases and milestone model

Each construction loan has a drawdown_schedule — a sequence of tranches, each with:

  • tranche_number — 1 = deposit/slab, 2 = frame, 3 = lock-up, 4 = fixing, 5 = completion (configurable per product)
  • tranche_amount — dollar amount or percentage of total facility
  • milestone_description — plain-text description, e.g. "Frame and roof complete"
  • status — lifecycle state: pendinginspection_requestedcertifieddrawnlapsed
  • certification_date and certifier_reference — quantity surveyor or building inspector reference
  • drawdown_date and posting_id — populated when funds are released

Milestone certification is provided by an approved quantity surveyor or building inspector. The bank does not self-certify. Certifier references are stored against each tranche and are available to auditors via the credit file.

Data model

-- credit.construction_schedules
CREATE TABLE credit.construction_schedules (
  schedule_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  loan_id            UUID NOT NULL REFERENCES credit.loans(loan_id),
  total_facility     NUMERIC(18,2) NOT NULL,
  total_drawn        NUMERIC(18,2) NOT NULL DEFAULT 0,
  construction_end_date DATE,  -- expected completion
  conversion_date    DATE,      -- when IO period ends and P&I begins
  status             TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','complete','defaulted')),
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- credit.construction_tranches
CREATE TABLE credit.construction_tranches (
  tranche_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  schedule_id        UUID NOT NULL REFERENCES credit.construction_schedules(schedule_id),
  tranche_number     INT NOT NULL,
  tranche_amount     NUMERIC(18,2) NOT NULL,
  milestone_description TEXT NOT NULL,
  status             TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','inspection_requested','certified','drawn','lapsed')),
  certification_date DATE,
  certifier_reference TEXT,
  drawdown_date      DATE,
  posting_id         UUID,
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (schedule_id, tranche_number)
);

Key operations

Drawdown request. Agent or customer submits a drawdown request for a tranche. The system validates: tranche status is certified, all prior tranches have status drawn, and the loan is not in arrears. On validation, the drawdown is posted to the ledger via MOD-001, total_drawn is incremented, tranche status moves to drawn, and the event bank.credit.construction_drawdown_posted is emitted. MOD-063 dispatches a drawdown confirmation to the customer including the updated drawn balance.

Milestone certification. Agent uploads the certification document (stored via MOD-073 if document management is available) and records certification_date and certifier_reference. Tranche status moves to certified. MOD-063 notifies the customer that the milestone has been verified and a drawdown can now be requested.

Interest accrual on drawn balance only. MOD-005 receives total_drawn as the accrual base, not total_facility. This value is refreshed after each drawdown posting. The customer is not charged interest on committed but undrawn funds. CON-005 compliance is maintained by design — there is no mechanism to accrue on the full facility.

LVR recalculation. After each drawdown, MOD-115 is called with the updated total_drawn value. LVR is recalculated as total_drawn / current_valuation. If LVR exceeds the policy breach threshold, an alert is generated and routed to the credit team for review. The property security record is updated with the new drawn balance so that LVR history is visible in the collateral register.

Completion and conversion. When all tranches reach status drawn, or when construction_end_date is reached (whichever is first), the schedule status is set to complete. MOD-112 is triggered to generate the full P&I amortisation schedule beginning on conversion_date. MOD-063 dispatches a notification to the customer containing the first repayment date, monthly repayment amount, and remaining loan term.

Requirements

ID Requirement
FR-545 System shall prevent drawdown disbursement unless the corresponding tranche has status certified.
FR-546 System shall post each approved drawdown as a ledger debit via MOD-001 and update total_drawn atomically.
FR-547 System shall supply total_drawn (not total_facility) to MOD-005 as the interest accrual base after each drawdown.
FR-548 System shall trigger MOD-112 to generate a P&I amortisation schedule when all tranches reach status drawn or construction_end_date is reached.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy GATE Each drawdown tranche requires a completed milestone certification before funds are released — the system will not release funds based on a schedule alone.
CON-004 — Product Disclosure & Sales Practice Policy AUTO The customer receives an updated amortisation schedule after each drawdown, reflecting the new principal balance and any change in the interest-only period remaining.
CON-005 — Fee & Pricing Transparency Policy CALC Interest accrues only on the drawn balance — not the total approved facility — ensuring the customer is not charged interest on undrawn funds.
CRE-001 — Credit Risk Management Policy CALC LVR is recalculated after each drawdown using the current drawn balance against the most recent valuation, with the result fed to MOD-115.

MOD-128 — Credit bureau enquiry and CCR integration

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Integrates with external credit reporting bureaus to retrieve credit reports and scores for applicants and existing customers. Supports comprehensive credit reporting (CCR) in AU and standard credit reporting in NZ. Acts as the single integration point for all bureau calls, enforcing consent, managing duplicate enquiry suppression, and routing to the correct bureau by jurisdiction.

Regulatory context

Australia — CCR regime. The Comprehensive Credit Reporting regime, enacted under the Privacy Act 1988 and the Credit Reporting Code, requires participating credit providers to report repayment history information to bureaus and entitles them to receive the same in return. Positive and negative data are both reported and received. The regime mandates that enquiry consent is obtained before placing a bureau call, and that the type of enquiry (credit assessment, account review, collection) is disclosed.

New Zealand. The Credit Reporting Privacy Code 2004 (under the Privacy Act 2020) governs credit reporting in NZ. Consumer credit reporters must obtain consent before accessing a credit report. Enquiry purposes are constrained: credit assessment, account review, and collection are the permitted purposes. Adverse credit information — missed payments, defaults, judgements, bankruptcies — is the primary content. NZ does not operate a CCR regime (no positive repayment history is reported); however, some lenders are in the early stages of voluntary positive reporting under the new Privacy Act framework.

Bureau coverage

Jurisdiction Bureaus supported
AU Equifax AU, Experian AU, illion
NZ Centrix, Equifax NZ

The tenant configuration (credit.bureau_config) specifies which bureau(s) to call per jurisdiction and the priority order for multi-bureau strategies. The default is single-bureau primary with a fallback, configurable per product type.

Duplicate enquiry suppression

A bureau call creates a "hard enquiry" on the customer's credit file, which is visible to other lenders and can marginally affect the customer's credit score. Unnecessary duplicate enquiries are harmful to the customer and can indicate a poorly controlled credit process to regulators.

The module enforces a 30-day suppression window: if a bureau report for the same customer and the same enquiry type already exists in credit.bureau_enquiries with created_at within the last 30 days, the module returns the cached report rather than placing a new bureau call. The suppression window is configurable per product type — it may be shortened for high-velocity credit products.

Data model

-- credit.bureau_enquiries
CREATE TABLE credit.bureau_enquiries (
  enquiry_id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id        UUID NOT NULL,
  application_id     UUID,  -- null for account review enquiries
  jurisdiction       TEXT NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  bureau             TEXT NOT NULL,  -- 'equifax_au', 'experian', 'illion', 'centrix', 'equifax_nz'
  enquiry_purpose    TEXT NOT NULL CHECK (enquiry_purpose IN ('credit_assessment','account_review','collection')),
  consent_reference  UUID NOT NULL,  -- reference to the consent record
  request_at         TIMESTAMPTZ NOT NULL DEFAULT now(),
  response_at        TIMESTAMPTZ,
  response_status    TEXT CHECK (response_status IN ('success','no_file','bureau_error','timeout')),
  credit_score       INT,
  bureau_reference   TEXT,
  report_payload     JSONB,  -- encrypted at rest; never logged in plaintext
  adverse_flags      TEXT[],  -- ['default','judgement','bankruptcy','missed_payments']
  suppressed         BOOLEAN NOT NULL DEFAULT false,  -- true if returned from cache
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

report_payload is stored encrypted at rest and is never written to application logs. Access to the raw payload is restricted to the credit decisioning engine (MOD-029) and authorised back-office credit assessors. Adverse flag codes are stored as structured arrays separately from the full payload to allow downstream filtering without requiring decryption.

Key operations

Before placing any bureau call, the module checks that a consent record exists in the consent management layer for the customer, with purpose = 'credit_bureau_enquiry' and created_at within the consent validity window (typically the current credit application session). If no valid consent exists, the call is blocked and an error is returned to the calling module. The consent record reference is stored on the enquiry.

Bureau call routing

The module selects the bureau based on the account's jurisdiction and the tenant's bureau configuration. For AU credit assessment: the primary bureau (e.g. Equifax AU) is called first. If the primary bureau returns no_file or errors, the fallback bureau (e.g. illion) is called. For NZ: typically Centrix for consumer lending, Equifax NZ for mortgage applications — configurable by product type.

Duplicate suppression

Before placing a live call, the module checks for a non-suppressed enquiry for the same customer_id and enquiry_purpose in the suppression window. If found, it returns the existing report with suppressed = true. This reduces hard enquiry count on the customer's file and avoids unnecessary bureau API costs.

Report delivery

On successful response, the report payload is stored encrypted, adverse flags are parsed and stored in structured format, and the credit score (if returned) is stored. The enquiry status is set to success or no_file. The calling module (MOD-029) receives the structured adverse flags and score — not the raw payload — via the internal API. The raw payload is only accessible to authorised credit assessors via the back-office panel.

Adverse finding disclosure

If adverse_flags is non-empty and the bureau data contributed to an unfavourable credit decision, MOD-050 is called to include the adverse finding summary in the pre-decision disclosure provided to the applicant. The applicant's right to access their credit report from the bureau is disclosed in the same communication.

Requirements

FR-577 — Consent enforcement: the module must verify that a valid bureau enquiry consent record exists before placing any call to a bureau API; if consent is absent or expired, the call must be blocked and an error returned to the calling module — no bypass path may exist.

FR-578 — Duplicate enquiry suppression: the module must check for an existing bureau report for the same customer and enquiry purpose within the 30-day suppression window before placing a new call; if a recent report exists, it must be returned from cache with suppressed = true rather than placing a duplicate hard enquiry.

FR-579 — Multi-bureau fallback: for AU credit assessments, the module must support a primary and fallback bureau configuration per product type; if the primary bureau returns no_file or a timeout, the module must automatically route to the fallback bureau without manual intervention.

FR-580 — Adverse finding disclosure: if an enquiry returns non-empty adverse_flags and those flags contribute to an unfavourable credit decision, the module must call MOD-050 to deliver an adverse action disclosure to the applicant before the decision is communicated, including the bureau name, the nature of the adverse information, and the applicant's right to access their credit report.

Policies satisfied:

Policy Mode Description
CRE-003 — Credit Decisioning & Scorecard Policy GATE A credit enquiry must be completed and the bureau response recorded before the credit decision engine (MOD-029) can proceed to assessment — no decision is made without current bureau data.
PRI-001 — Privacy Policy GATE Bureau enquiries are made only with the applicant's explicit consent, recorded in the consent management layer before any call is placed to the bureau API.
CON-004 — Product Disclosure & Sales Practice Policy LOG Adverse bureau findings (score, adverse flags, bureau name) are captured on the enquiry record and returned structured to the caller; the calling decisioning module (MOD-029) is responsible for invoking MOD-050 to deliver disclosure to the applicant.
REP-010 — Credit reporting & bureau submission LOG All bureau enquiries — request, response, bureau reference, and consent record — are logged for regulatory examination and to support hardship review processes.

MOD-132 — Loan restructure and variation workflow

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Manages the lifecycle of customer-initiated changes to the terms of an existing loan. A loan variation covers repayment frequency changes, switching between variable and fixed interest rates, extending the loan term, restructuring repayments, and capitalising arrears. Loan variations are a routine commercial product feature — distinct from financial hardship assistance (MOD-139) — and must be governed by a structured workflow that ensures regulatory re-disclosure and credit re-assessment where the variation is material.

Regulatory context

CCCFA (NZ) s108 and NCCP (AU) require the bank to re-disclose revised contract terms whenever a loan variation materially changes the obligations of the borrower. Where a variation increases the credit exposure — such as extending the term or capitalising arrears — the responsible lending obligation under s9C CCCFA and s130 NCCP also requires the bank to assess whether the variation is unsuitable for the borrower given their current financial position. This creates two distinct regulatory gates: a disclosure gate (every variation) and a creditworthiness gate (material variations only).

Break cost disclosure under CCCFA and the bank's own consumer credit policies requires that any cost the customer will incur by breaking a fixed rate period is calculated and acknowledged before the variation is confirmed. This is a pre-condition, not a post-event notification.

Variation types

Variation type Credit reassessment required Break cost disclosure required Notes
Term extension > 12 months Yes No Increases total interest payable — material change
Term extension ≤ 12 months No No Minor adjustment — disclosure only
Repayment frequency change No No e.g. monthly to fortnightly — disclosure only
Rate type switch (variable → fixed) No No No increase in exposure — disclosure only
Rate type switch (fixed → variable) No Yes Break cost on fixed period exit
Early repayment (partial or full) No Yes Break cost on fixed period exit
Capitalisation of arrears Yes No Increases principal — material change
Repayment restructure Yes No Extends effective term or reduces repayment amount

Assessment rules

The system determines materiality by comparing the variation type against the configured materiality rules at the time of request. The two assessment paths are:

Creditworthiness gate path. Term extensions > 12 months, rate type switches that increase the balance or term, capitalisation of arrears, and repayment restructures that reduce the scheduled repayment amount invoke MOD-029. The variation is held in assessed status pending the outcome. A declined assessment rejects the variation and notifies the customer with the reason.

Disclosure-only path. Repayment frequency changes, minor term adjustments (≤ 12 months), and variable-to-fixed rate switches proceed directly to disclosure without a credit check. The revised schedule and terms are calculated, the disclosure is dispatched via MOD-050, and the variation is confirmed on acknowledgement.

Break cost gate. Independent of the above two paths, any variation involving exit from a fixed rate period invokes MOD-163 to calculate the break cost. The variation is held in disclosed status until the customer acknowledges the break cost figure via MOD-050. The acknowledgement reference is written to the variation record before the variation moves to confirmed.

Data model

-- credit.loan_variations
CREATE TABLE credit.loan_variations (
  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,
  proposed_terms        JSONB NOT NULL,
  status                TEXT NOT NULL DEFAULT 'requested' CHECK (status IN (
                          'requested', 'assessing', 'assessed', 'disclosed', 'confirmed', 'rejected', 'expired'
                        )),
  assessment_required   BOOL NOT NULL DEFAULT false,
  credit_check_id       UUID,            -- reference to MOD-029 assessment record
  disclosure_id         UUID,            -- reference to MOD-050 disclosure record
  break_cost_acknowledged BOOL NOT NULL DEFAULT false,
  acknowledgement_ref   TEXT,            -- break cost acknowledgement reference from MOD-050
  requested_by          UUID NOT NULL,   -- customer_id or agent_id
  rejection_reason      TEXT,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX ON credit.loan_variations (loan_id);
CREATE INDEX ON credit.loan_variations (status);

previous_terms and proposed_terms capture the full term snapshot — interest rate, repayment amount, repayment frequency, term end date, rate type, and any other material fields — so the before/after state is preserved without relying on the current loan record at time of audit.

Key operations

Request. Customer or agent submits a variation request specifying the variation type and proposed terms. The system creates a credit.loan_variations record with status requested and emits bank.credit.loan_variation_requested. The materiality rule engine evaluates the variation type and sets assessment_required.

Assessment gate. If assessment_required is true, MOD-029 is invoked with the customer's current financial profile and the proposed terms. The outcome is written to credit_check_id. An approved assessment moves status to assessed; a declined assessment moves status to rejected and closes the variation.

Break cost calculation. If the variation type requires break cost disclosure, MOD-163 calculates the cost and MOD-050 dispatches the disclosure. The variation is held in disclosed status. On acknowledgement by the customer, break_cost_acknowledged is set to true, acknowledgement_ref is populated, and status advances.

Disclosure dispatch. MOD-050 generates the revised disclosure document including the new repayment schedule from MOD-112, the new rate, the new term end date, and the total interest payable comparison. The disclosure_id is written to the variation record. The variation is held at disclosed until the customer confirms.

Confirmation. On customer confirmation, status moves to confirmed. MOD-112 regenerates the amortisation schedule and applies it to the loan. MOD-063 and MOD-050 deliver the updated schedule to the customer within 24 hours. The event bank.credit.loan_variation_confirmed is emitted.

Rejection. The variation can be rejected at any stage — by the credit engine, by the customer declining the disclosure, or by an agent. The rejection reason is recorded and status is set to rejected. All events up to that point remain in the log.

Requirements

ID Requirement
FR-589 System shall determine whether a requested loan variation requires credit reassessment by comparing the variation type against the configured materiality rules; material variations (term extension > 12 months, rate type switch, capitalisation of arrears) must invoke MOD-029 before proceeding; non-material variations (repayment frequency change, minor term adjustment) must proceed to disclosure without reassessment.
FR-590 System shall generate an updated amortisation schedule via MOD-112 for every confirmed loan variation and deliver it to the customer via MOD-063 and MOD-050 within 24 hours of the variation being confirmed, showing the revised repayment amount, new term end date, and any change in total interest payable.
FR-591 System shall enforce the break cost disclosure gate for any variation that involves early repayment or conversion of a fixed rate period — the variation must not be confirmed until the customer has acknowledged the break cost calculated by MOD-163 via MOD-050; the acknowledgement reference must be recorded on the variation record.
FR-592 System shall log every loan variation event — request, assessment decision, disclosure dispatch, customer confirmation, and any rejection — as an immutable record in credit.loan_variations with the requesting party, timestamp, and full before/after terms; the log must be available for regulatory examination without reconstruction.

Policies satisfied:

Policy Mode Description
CRE-001 — Credit Risk Management Policy GATE Material variations — term extension, rate type change, and capitalisation of arrears — require a fresh creditworthiness check via MOD-029 before the variation can proceed.
CON-004 — Product Disclosure & Sales Practice Policy AUTO An updated disclosure showing the revised repayment schedule, new rate, and any break cost is automatically issued to the customer via MOD-050 before the variation is confirmed.
CON-005 — Fee & Pricing Transparency Policy GATE Break cost disclosure must be acknowledged by the customer before any fixed-to-variable conversion or early repayment variation is processed — the variation is held until acknowledgement is recorded.
REP-004 — Financial Statements Policy LOG Every loan variation event — request, assessment decision, disclosure dispatch, customer confirmation, and rejection — is logged as an immutable record for regulatory examination and responsible lending audit.

MOD-162 — Loan facility & component manager

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Manages the parent facility entity and all component records for the Flexible Loan Facility (PRD-024). This module owns the core data model: one facility with one approved credit limit, and any number of components (fixed-rate or floating-rate) established within that limit. It enforces the limit constraint, maintains the principal-weighted effective rate, records all component lifecycle transitions immutably, and publishes events for downstream consumption.

Context

The Flexible Loan Facility requires a two-tier data model that existing single-loan modules do not support. A standard loan account (credit.loan_accounts in MOD-065) has one rate and one term; the FLF has a parent entity with a limit plus multiple children each with independent rates and terms. This module introduces two new tables — credit.loan_facilities (the parent) and credit.loan_facility_components (the children) — and the aggregation logic that collapses them into a customer-facing effective rate.

The synthetic swap book analogy from the product research document maps directly to this module's output: the treasury view of the aggregated fixed cash flows is simply a query across credit.loan_facility_components grouped by maturity bucket. No separate treasury module is needed to construct this view; it is derived from the component records.

Data model

credit.loan_facilities

Mutable parent entity. One row per facility.

Column Type Notes
id uuid PK
customer_id uuid NOT NULL
credit_decision_id uuid NOT NULL FK → credit.credit_decisions
facility_limit numeric(18,2) NOT NULL
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-weighted average, recomputed on component events
status text NOT NULL CHECK ('ACTIVE','EXPIRED','CANCELLED')
master_agreement_ref text NULL — document reference for the signed master agreement
trace_id text NULL
created_at timestamptz NOT NULL DEFAULT now()
last_updated timestamptz NOT NULL DEFAULT now()

credit.loan_facility_components

Append-only. New rows are inserted on every component event; terminal-state rows are Cat 1 immutable via trg_loan_facility_components_immutable (reuses credit.fn_immutable_row()). A component exists as a sequence of rows — the latest row is the current state.

Column Type Notes
id uuid PK
facility_id uuid NOT NULL FK → credit.loan_facilities
component_seq int NOT NULL — ordinal within the facility (1, 2, …)
component_type text NOT NULL CHECK ('FIXED','FLOATING')
principal_amount numeric(18,2) NOT NULL CHECK (> 0)
interest_rate numeric(8,6) NOT NULL — contracted rate for FIXED; current effective rate for FLOATING
rate_benchmark text NULL — 'BKBM' or 'BBSY'; present only for FLOATING
benchmark_margin numeric(8,6) NULL — customer margin over benchmark; present only for FLOATING
term_months int NULL — NULL for FLOATING (no fixed term)
start_date date NOT NULL
maturity_date date NULL — NULL for FLOATING
amortisation_type text NULL CHECK ('INTEREST_ONLY','PRINCIPAL_AND_INTEREST') — NULL for FLOATING
status text NOT NULL CHECK ('PENDING','ACTIVE','MATURED','PREPAID','CANCELLED')
trigger_reason text NOT NULL CHECK ('INITIAL_CREATION','ROLLOVER','RATE_REPRICING','PARTIAL_PREPAYMENT','FULL_PREPAYMENT','FACILITY_EXPIRY','MANUAL_ADMIN')
previous_component_id uuid NULL FK → self — set on rollover or partial prepayment replacing an earlier row
model_version text NOT NULL DEFAULT 'v1.0.0'
trace_id text NULL
created_at timestamptz NOT NULL DEFAULT now()

Immutability trigger fires on BEFORE UPDATE OR DELETE; throws an exception if the row's status is MATURED, PREPAID, or CANCELLED.

Index: (facility_id, status, maturity_date) for maturity sweep queries. (facility_id, component_seq, created_at DESC) for current-state lookups.

Handlers

create-facility — Invoked by MOD-029 via event or direct call after credit decision APPROVE. Creates the credit.loan_facilities row and the initial FLOATING component holding the full facility limit. Publishes bank.credit.facility_created.

create-component — Creates a new FIXED component by allocating principal from the floating residual. Validates: sum of all ACTIVE component principals ≤ facility_limit; component principal ≥ configured minimum. Calls MOD-112 to generate the amortisation schedule. Recomputes effective rate. Publishes bank.credit.component_created.

daily-maturity-sweep — EB Scheduler cron, runs at 06:00 NZST. Transitions ACTIVE components whose maturity_date = CURRENT_DATE to MATURED. Creates a new FLOATING component absorbing the matured principal if no rollover was elected. Publishes bank.credit.component_status_changed. DISABLED in non-prod.

reprice-floating — Consumes bank.core.rate_changed from MOD-006. Updates the effective rate on the FLOATING component. Recomputes facility effective rate. Publishes bank.credit.effective_rate_changed.

update-component-status — Called by MOD-163 after a binding break-cost acknowledgement is confirmed. Transitions a FIXED component to PREPAID or updates its principal on partial prepayment. Increases the floating residual. Recomputes effective rate.

Effective rate computation

On every component event, the effective interest rate is computed as:

effective_rate = Σ(component.principal_amount × component.interest_rate) / Σ(component.principal_amount)

where the sum is over all ACTIVE components for the facility. This is persisted to credit.loan_facilities.effective_interest_rate within the same transaction as the component write. It is used for disclosure (CON-004), for IFRS 9 EIR computation (MOD-031), and for customer-facing statements (MOD-113).

Events published

Event Bus Trigger
bank.credit.facility_created bank-credit Facility established
bank.credit.component_created bank-credit New component added to facility
bank.credit.component_status_changed bank-credit Component transitions to MATURED, PREPAID, or CANCELLED
bank.credit.effective_rate_changed bank-credit Effective rate changes on any component event

Consumers: MOD-030 (IFRS 9 stage allocation on facility_created), MOD-163 (component data for break-cost calculations), MOD-042 (CDC ingestion).

Implementation notes

The floating component is not a fixed-term obligation; it absorbs the unallocated portion of the facility limit at all times. Its principal changes whenever a fixed component is created (reduces floating), a fixed component matures or is prepaid (increases floating), or a partial prepayment of the floating leg itself occurs. Its interest rate changes with MOD-006 rate-change events.

The facility limit constraint must be enforced transactionally: the DB must have a row-level check or the handler must use a SELECT FOR UPDATE on the facility row before inserting a new component. Race conditions where two concurrent component-creation requests both pass the in-memory check are not acceptable given the credit limit is a regulatory obligation.

The model_version field on each component row records the version of the pricing and aggregation logic in effect when the row was written. This supports retrospective investigation of effective-rate discrepancies.

Policies satisfied:

Policy Mode Description
CRE-002 — Responsible Lending Policy AUTO Component creation is blocked when the sum of component principals would exceed the approved facility limit — no code path creates a component outside the approved credit envelope; enforced at the handler layer and by a DB CHECK constraint.
CON-004 — Product Disclosure & Sales Practice Policy AUTO Principal-weighted effective interest rate is recomputed and persisted to the facility record within 500 ms of every component event, making the current effective rate available for disclosure at all times.
REP-004 — Financial Statements Policy AUTO Component-level cash flow data and stage transition events are published on every lifecycle change, feeding the IRRBB repricing gap view and ECL stage-allocation inputs automatically.

MOD-163 — Break-cost calculator

System: SD05 | Repo: bank-credit | Build status: Deployed | Deployed: Yes

Purpose

Calculates the break cost or benefit when a fixed-rate loan component is prepaid, rolled over before maturity, or otherwise terminated before its contracted end date. Serves two call paths: on-demand indicative quotes (available to the customer at any time without triggering account action) and binding calculations (required by MOD-050 before any component change proceeds). Every calculation is immutably logged.

Context

The break-cost calculation is the single most important conduct and risk management feature of the Flexible Loan Facility. It is the mechanism by which the bank recovers (or passes back) the mark-to-market movement on the interest-rate position it hedged when the customer locked a rate. Getting this wrong — whether through formula error, stale market rates, or inconsistency between indicative and binding figures — creates both regulatory exposure and the specific conduct risk evidenced by the UK Tailored Business Loans episode.

This module implements the formula once, in a single service that is called by all paths — indicative, binding, and any future batch re-valuation. There is no separate or simplified formula for any call path.

Formula

break_cost = Σ_t [ cash_flow_t × (contracted_rate − current_market_rate) / (1 + discount_rate)^t ]

In the simplified annuity form used for standard components:

break_cost = (contracted_rate − current_market_rate) × outstanding_principal × annuity_factor(discount_rate, remaining_months)

Where: - contracted_rate — the fixed rate locked at component establishment, from credit.loan_facility_components - current_market_rate — the mid-market swap rate for the residual tenor in months, sourced from MOD-085 at the moment of calculation - outstanding_principal — the current outstanding principal of the component - discount_rate — the current market rate for the component tenor (same as current_market_rate for a par-swap valuation) - remaining_months — months between calculation date and the component maturity date (floor: 0)

A positive result means the customer pays the bank (rates have fallen since fixing). A negative result is a break benefit; the bank pays the customer in full per CRE-009.

Formula version is stored on every calculation row as formula_version (e.g. v1.0.0). A formula change requires a version increment.

Data model

credit.break_cost_calculations

Cat 1 immutable via trg_break_cost_calculations_immutable (ADR-048, reuses credit.fn_immutable_row()). One row per calculation call.

Column Type Notes
id uuid PK
component_id uuid NOT NULL FK → credit.loan_facility_components
facility_id uuid NOT NULL FK → credit.loan_facilities
customer_id uuid NOT NULL
calculation_type text NOT NULL CHECK ('INDICATIVE','BINDING')
contracted_rate numeric(8,6) NOT NULL
current_market_rate numeric(8,6) NOT NULL — rate sourced from MOD-085
market_rate_tenor_months int NOT NULL
market_rate_source text NOT NULL — identifies the MOD-085 feed and instrument
market_rate_timestamp timestamptz NOT NULL — timestamp of the rate from MOD-085
outstanding_principal numeric(18,2) NOT NULL
remaining_months int NOT NULL
discount_rate numeric(8,6) NOT NULL
break_cost_amount numeric(18,2) NOT NULL — positive = customer pays; negative = bank pays benefit
currency char(3) NOT NULL
formula_version text NOT NULL DEFAULT 'v1.0.0'
acknowledgement_id text NULL — set for BINDING type once MOD-050 acknowledgement is confirmed
acknowledged_at timestamptz NULL
calculated_by text NOT NULL CHECK ('CUSTOMER','SYSTEM','ADMIN')
trace_id text NULL
calculated_at timestamptz NOT NULL DEFAULT now()

Index: (component_id, calculation_type, calculated_at DESC) for current binding calculation lookup.

Handlers

calculate-indicative — Synchronous API callable by MOD-164 (customer-facing) and any internal caller. Reads component data from MOD-162, fetches current market rate from MOD-085 for the residual tenor, runs the formula, inserts a calculation_type='INDICATIVE' row, and returns the result. No account action is triggered. Response p99 ≤ 500 ms (NFR-007 applies).

calculate-binding — Invoked when a customer confirms intent to terminate or roll a fixed-rate component early. Runs the same formula but inserts calculation_type='BINDING'. Returns the calculation_id. The caller (MOD-164 or MOD-132) passes calculation_id to MOD-050 to trigger the break-cost acknowledgement disclosure. The component cannot be changed until MOD-050 returns a confirmed acknowledgement ID.

confirm-acknowledgement — Called by MOD-050 when the customer acknowledges the binding disclosure. Updates the binding row with acknowledgement_id and acknowledged_at. Emits bank.credit.break_cost_acknowledged for MOD-162 to action the component change.

Market rate staleness

The MOD-085 rate feed has a configured maximum age. If the cached rate is older than the staleness threshold (default: 15 minutes), calculate-indicative returns a RATE_STALE warning alongside the result; calculate-binding returns a RATE_STALE error and does not produce a binding calculation until a fresh rate is available. Binding calculations on stale rates are prohibited — the customer could be shown an incorrect break cost and acknowledge a figure that does not reflect the actual unwind cost.

Break benefit

When break_cost_amount is negative, the result is a break benefit. CRE-009 requires the bank to pass this benefit to the customer in full. The binding calculation handler, when producing a break benefit, sets a benefit_payable flag in the bank.credit.break_cost_acknowledged event. MOD-001 is responsible for posting the benefit credit to the customer's account via the standard payments path. The break-cost calculator does not post to the general ledger directly.

Consistency with MOD-116

MOD-116 (Mortgage servicing engine) uses the same break-cost formula for simple fixed-rate mortgage components. Both modules source market rates from MOD-085 and use the same PV formula. In v1, each module maintains its own implementation. A shared calculation library (@bank/break-cost) should be extracted as a v2 improvement to eliminate the duplicate and ensure formula changes are applied atomically across both modules.

Implementation notes

The formula must be deterministic for any given set of inputs. Given the same contracted rate, market rate, principal, remaining term, and discount rate, the formula must return the same result regardless of when or by whom it is called. This is a testability requirement (pure function in the core calculation service) and a regulatory requirement (CRE-009 formula consistency).

The market_rate_tenor_months must match the component's actual remaining term to the nearest available tenor in the MOD-085 rate curve, not the original component term. If the remaining term falls between two available tenors on the curve, interpolate linearly. Document the interpolation method in the design doc.

Policies satisfied:

Policy Mode Description
CRE-009 — Fixed-Rate Component Break-Cost Methodology Policy CALC Break cost is computed using the documented present-value formula against live market rates sourced from MOD-085; every calculation — indicative and binding — is immutably logged with contracted rate, current market rate, source timestamp, remaining principal, remaining term, and the resulting amount; the formula is version-controlled and produces identical results to any customer-facing quotation.
CON-005 — Fee & Pricing Transparency Policy GATE The binding break-cost calculation is delivered to the customer via MOD-050 for acknowledgement before any fixed-rate component early termination or pre-maturity rollover is processed; the component status handler verifies the acknowledgement record ID before proceeding; no code path bypasses this gate.

MOD-167 — Credit card facility engine

System: SD05 | Repo: bank-credit | Build status: Not started | Deployed: No

What this module does

Credit card facility engine. Manages the full lifecycle of a revolving credit card facility from opening through to closure: balance tracking, billing cycles, interest accrual, minimum repayment calculation, statement generation, and the real-time JIT-funding webhook that sits in the card-authorisation path.

This module ID is reserved per ADR-058 (credit card platform boundary). It is not built until a credit card product is formally greenlit by the CEO/Board (ADR-058 Phase 2).

Why it exists

ADR-058 established the credit card platform boundary and determined that the credit card facility engine is the only substantive new platform module required to launch a credit card programme. All other boundary decisions — scheme/processor abstraction (PAY-008), physical card records (MOD-124 credit-ready columns), and credit decisioning (MOD-029/031) — have already been addressed as pre-work.

The module ID is reserved here so that cross-references in ADR-058, PAY-008, and MOD-124 (credit_facility_id column) are stable and do not drift to an unrelated module if the number is allocated in the interim.

Responsibilities

When built, this module owns:

  • Facility management — open, limit changes, suspension, closure of revolving credit facilities. Maps to a credit.loan_accounts row with product_type = 'CREDIT_CARD'.
  • Balance tracking — real-time available credit = facility limit − outstanding balance − pending authorisations.
  • Billing cycle engine — monthly cycle cut, statement balance, minimum repayment calculation per NZ and AU regulatory requirements (minimum of 2% of outstanding or $25, whichever is greater, per jurisdiction rules).
  • Interest accrual — daily interest on the revolving balance at the applicable rate; posted via MOD-001 posting_type = 'ACCRUAL'.
  • JIT-funding webhook — the only platform component sitting in the sub-100ms card-authorisation path. Receives authorisation requests from the issuer-processor, checks available credit in-process (no external I/O in the response path), and responds with approve/decline. Performance target: p99 < 80ms under steady-state load (PAY-008 mandate).
  • Statement generation — monthly statements via MOD-113 (or internally until MOD-113 is extended for the credit card asset class).
  • Minimum repayment reminders — dispatched via MOD-063 at billing cycle close.
  • IFRS 9 integration — revolving balance feeds MOD-031 ECL calculation as the EAD for the CREDIT_CARD product type.

Pre-work already done (ADR-058)

The following changes were made before this module is built:

  1. MOD-124payments.physical_cards extended with card_type = 'credit' option, credit_facility_id UUID (links to this module's facility record), and bin_range_type (sponsor | principal).
  2. PAY-008 — Card scheme abstraction policy extended with processor-neutral interface, scheme-neutral BIN range configuration, JIT-funding webhook p99 < 80ms mandate, and processor-switch covenant (PAN/token portability as RFP non-negotiable).

What triggers the build

A formal credit card product greenlight decision by the CEO/Board, followed by selection of an issuer-processor and card scheme. ADR-058 Phase 2 is the trigger. Until then this module remains Not started and deployed: false.

Design notes (pre-greenlit)

  • Follows the MOD-117 (overdraft management) pattern for revolving product management within the SD05 credit domain.
  • The JIT-funding webhook must be architecturally isolated from the rest of the module — it must not share a Lambda with any path that calls external services (database reads should be warm-cache or in-memory for the authorisation response).
  • NZ and AU minimum repayment rules differ; jurisdiction-aware billing cycle logic is required from day one.
  • BIN range sponsorship is a distinct credit-card-specific relationship from the debit/eftpos sponsorship (PAY-008 §BIN range sponsorship). This requires separate negotiation with a sponsor bank that holds credit-card BIN sponsorship capability.

Policies satisfied:

Policy Mode Description
CRE-001 — Credit Risk Management Policy AUTO Credit card revolving facility balance and utilisation are tracked per customer account with full audit trail for credit risk reporting.
CRE-006 — Impairment & Provisioning Policy CALC Credit card asset class contributes to IFRS 9 ECL calculations — revolving balance is the EAD; product-type LGD and PD are applied by MOD-031 once MOD-167 feeds the credit.loan_accounts table.
CON-004 — Product Disclosure & Sales Practice Policy GATE Minimum repayment disclosures and billing-cycle statements are gated on delivery confirmation before the next billing cycle opens.
PAY-008 — Payment Routing, Sponsor & Card-Scheme Abstraction Policy AUTO JIT-funding webhook responds to issuer-processor authorisation requests within p99 < 80ms by querying available credit in-process with no blocking external I/O.

SD06 — Snowflake Analytics & Risk Platform

Repo: bank-risk-platform | Business domain: BD03 | Tech owner: Data & Risk Engineering | Build status: Not started

The bank's analytical brain. All risk models, regulatory calculations, and intelligence outputs run in Snowflake. Results written back to Postgres for operational use. No manual spreadsheet calculations.

Architecture

See ADR-002 for the Snowflake-as-analytical-store decision and the write-back pattern that governs how results flow to operational systems.

Critical constraints

  1. Snowflake is the analytical store only — no operational system may query Snowflake inline during a customer request.
  2. All risk model outputs must be written back to Postgres before operational systems consume them.
  3. MOD-036 prudential returns must be submitted by RBNZ/APRA deadlines without manual intervention.
  4. MOD-038 must block downstream model runs if data quality checks fail.

Deployment notes — as of 2026-05-14

Most Phase 3–4 modules (MOD-032, MOD-035, MOD-038, MOD-039, MOD-040, MOD-085, MOD-086, MOD-098) are deployed to dev as of commit 629c644.

Three modules remain undeployed. The shared AWS SCP blocker was resolved (bank-platform commit 911a11f7). Diagnostic update 2026-05-14: SHOW GRANTS (live dev account, ACCOUNTADMIN) confirmed BANK_NONPROD_RISK_ROLE was already granted to BANK_RISK_PLATFORM_DEPLOY since 2026-04-30. The earlier diagnosis attributing MOD-056/080 failures to a missing grant was incorrect. The real blocker on MOD-056/080 is that HAS_DCM=false was set as a workaround, preventing Phase-2 ownership transfer from ever running against real credentials:

  • MOD-041HAS_DCM=false / HAS_DBT_PROJECT=false workaround skips the Snowpark deploy step, so NORMALISE_MERCHANT UDF is never deployed. Resolution: flip HAS_DCM=true and HAS_DBT_PROJECT=true — the Snowpark path should deploy the UDF automatically once the flags are restored.

  • MOD-056HAS_DCM=false workaround prevented Phase-2 ownership transfer from running. After flipping HAS_DCM=true, the expected next blocker is schema ownership: the REGULATORY schema is owned by BANK_NONPROD_RISK_ROLE, not SF_ROLE; dbt creating views in that schema as SF_ROLE will fail with "Insufficient privileges". Resolution options: (A) extend Phase-2 in risk-platform.gitlab-ci.yml to also transfer schema ownership, or (B) add explicit GRANT CREATE VIEW / DYNAMIC TABLE ON SCHEMA REGULATORY TO ROLE SF_ROLE.

  • MOD-080 — Same Phase-2 / schema-ownership issue as MOD-056 (schema STATUTORY), plus an independent code bug: reconciliation_status_current Dynamic Table has target_lag='5 minutes' while its upstream dependency trial_balance_period has target_lag='15 minutes' — Snowflake requires downstream DTs to have lag ≥ their dependencies. Fix: bump reconciliation_status_current.sql target_lag to '15 minutes' (independent of the Phase-2 / schema-ownership fix).

Modules in SD06


MOD-032 — LCR / NSFR calculator

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Snowflake dynamic tables computing LCR and NSFR from live ledger positions. Intraday and end-of-day snapshots. See ADR-002.

Streamlit dashboard

MOD-032 ships a Streamlit page RISK_CAPITAL.STREAMLIT_LIQUIDITY_DASHBOARD providing: - Current LCR: HQLA breakdown, net cash outflow components, ratio with RAF threshold indicator - Current NSFR: available and required stable funding, ratio - 30-day history charts for both ratios - Intraday exposure view (when intraday extension is deployed)

Consumed by MOD-171 (Risk Intelligence Dashboard) in the liquidity section. Cross-schema SELECT grant on RISK_CAPITAL.* published views required for RISK_INTELLIGENCE_ROLE.

Policies satisfied:

Policy Mode Description
CLQ-002 — Liquidity Risk Management Policy CALC LCR and NSFR calculated from real data continuously — no manual spreadsheet, no T+1 lag
REP-002 — Prudential Reporting Policy CALC Regulatory liquidity returns sourced from the same calculation used for internal monitoring
GOV-002 — Risk Appetite Statement Policy ALERT LCR breach of RAF threshold triggers automatic escalation — no reliance on manual monitoring

MOD-033 — RWA & capital ratio engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Applies Basel III standardised risk weights to all exposures. Computes CET1, Tier 1, and Total Capital ratios. Updated daily.

Streamlit dashboard

MOD-033 ships a Streamlit page RISK_CAPITAL.STREAMLIT_CAPITAL_DASHBOARD providing: - CET1, Tier 1, Total Capital ratios with RAF threshold indicators - RWA breakdown by exposure class (corporate, retail, sovereign, securitisation, operational) - Period-over-period comparison (current vs. prior quarter) - Capital headroom to minimum regulatory requirement

Consumed by MOD-171 (Risk Intelligence Dashboard) in the capital section. Cross-schema SELECT grant on RISK_CAPITAL.* published views required for RISK_INTELLIGENCE_ROLE.

Policies satisfied:

Policy Mode Description
CLQ-001 — Capital Adequacy Policy CALC Capital ratios computed from live exposure data — no manual risk weight application
REP-002 — Prudential Reporting Policy CALC APRA ARS / RBNZ BS returns populated from the same RWA engine — single source of truth
CLQ-006 — Capital Disclosure & Reporting Policy CALC Pillar 3 disclosure figures sourced from the same engine — no reconciliation gap
GOV-002 — Risk Appetite Statement Policy ALERT Capital ratio breach (CET1, Tier 1, or Total Capital) triggers an alert to CFO and CRO via MOD-076 alarm-intake, distinguishing between the regulatory minimum and the internal management buffer — FR-207.

MOD-034 — Stress testing scenario engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Applies regulator-defined and internal stress scenarios to balance sheet. Computes capital and liquidity impact. Feeds ICAAP and recovery plan.

Policies satisfied:

Policy Mode Description
CLQ-003 — Capital Planning & Stress Testing Policy CALC Stress test outputs documented and auditable — scenario inputs, model version, and results all logged
CLQ-005 — Internal Capital Adequacy Assessment Process (ICAAP) Policy CALC ICAAP stress test section populated from engine output — no manually assembled spreadsheet
GOV-002 — Risk Appetite Statement Policy CALC Stress scenarios include RAF threshold breach — recovery plan triggers identified automatically

MOD-035 — IRRBB / EVE / NII model

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Computes Economic Value of Equity and Net Interest Income sensitivity across rate shock scenarios. Repricing gap analysis by time bucket and currency.

Streamlit dashboard

MOD-035 ships a Streamlit page RISK_CAPITAL.STREAMLIT_IRRBB_DASHBOARD providing: - EVE sensitivity under all six standard interest rate shock scenarios (+200bp, -200bp, +100bp, -100bp, twist, parallel) - NII sensitivity over 12-month horizon per scenario - Scenario comparison chart with prior period overlay - Limit headroom for each scenario against board-approved IRRBB limits

Consumed by MOD-171 (Risk Intelligence Dashboard) in the IRRBB sensitivity section. Cross-schema SELECT on RISK_CAPITAL.* published views required for RISK_INTELLIGENCE_ROLE.

Policies satisfied:

Policy Mode Description
CLQ-004 — Interest Rate Risk in the Banking Book (IRRBB) Policy CALC IRRBB metrics computed from live balance sheet positions — not a quarterly exercise
REP-002 — Prudential Reporting Policy CALC IRRBB disclosures populated from model output — consistent with internal monitoring
GOV-002 — Risk Appetite Statement Policy ALERT EVE sensitivity breach of limit triggers automatic alert to ALCO and CRO

MOD-036 — Prudential return builder (RBNZ / APRA)

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Dynamic tables producing RBNZ BS-series and APRA ARS returns from live data. Cell-level data lineage. Validation rules gate submission. See ADR-013.

Approval gate (FR-807)

This module does not submit automatically. The submission orchestrator checks REGULATORY.RETURN_APPROVALS before posting to any regulator endpoint. If no approval record exists for the current (run_id, return_code), the orchestrator aborts and alerts the Finance team. The approval is written by MOD-170 (Regulatory Submissions Portal) — a Finance officer reviews the assembled return cell-by-cell in the portal Streamlit and clicks Approve. No code path in this module can bypass the gate.

REGULATORY.RETURN_APPROVALS (new table — V004)

Append-only. NFR-024 — no UPDATE or DELETE grants.

Column Type Constraints Notes
approval_id uuid PRIMARY KEY DEFAULT gen_random_uuid()
run_id uuid NOT NULL REFERENCES RETURN_RUNS(run_id)
return_code text NOT NULL RBNZ BS2A / BS13 / APRA ARS 110.0 / 210.0
jurisdiction text NOT NULL CHECK (jurisdiction IN ('NZ','AU'))
approving_officer_id text NOT NULL Snowflake current_user() at time of approval — must differ from system assembler
sign_off_reason text NOT NULL Mandatory comment from approving officer
action text NOT NULL CHECK (action IN ('APPROVED','REJECTED'))
actioned_at timestamptz NOT NULL DEFAULT current_timestamp()

Grants: SELECT, INSERT to REGULATORY_SUBMISSIONS_PORTAL_ROLE (MOD-170). SELECT to BANK_DBT_ROLE. No UPDATE or DELETE to any role.

Streamlit page

MOD-036 ships a Streamlit page REGULATORY.STREAMLIT_RETURN_BUILDER providing: - Cell-level return viewer for each assembled return (line items + source lineage + prior-period comparison) - Validation error detail view - Approval / rejection form writing to RETURN_APPROVALS

This Streamlit is the primary UI. It is embedded in the MOD-170 Regulatory Submissions Portal as the per-return detail view. Authorised roles: finance.regulatory_reporting, finance.senior_officer, compliance.officer.

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy AUTO Regulatory returns produced automatically on schedule — no manual data assembly
REP-002 — Prudential Reporting Policy LOG Every figure in every return traceable to source ledger entry — data lineage maintained
REP-005 — Data Quality & Assurance Policy GATE Submission is gated by two sequential checks — automated validation rules (data quality) AND an explicit Finance officer approval record in REGULATORY.RETURN_APPROVALS; the submission orchestrator refuses to post to RBNZ or APRA unless both gates are cleared.

MOD-037 — AUSTRAC / RBNZ AML reporting pipeline

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Collates SAR/STR submissions, IFTI/CMIR reports, and annual AML compliance data. Formats and submits automatically.

Approval gate (FR-807)

This module does not submit automatically. The submission orchestrator checks REGULATORY.RETURN_APPROVALS before posting to AUSTRAC or RBNZ. If no approval record exists for the current (run_id, return_code), the orchestrator aborts and alerts the Compliance team. The approval is written by MOD-170 (Regulatory Submissions Portal) — a Compliance officer reviews the assembled report in the portal Streamlit and clicks Approve. No code path in this module can bypass the gate.

Policies satisfied:

Policy Mode Description
REP-003 — AML Compliance Reporting Policy AUTO AML reporting obligations discharged automatically — no manual submission process
AML-001 — AML/CFT Programme Policy AUTO Annual AML compliance report data sourced from operational systems — no manual collation
AML-006 — Suspicious Activity Reporting Policy LOG SAR submissions tracked from creation to acknowledgement — no submission gaps possible
REP-005 — Data Quality & Assurance Policy GATE Submission is gated by Finance officer approval in REGULATORY.RETURN_APPROVALS (via MOD-170); the submission orchestrator aborts if no approval record exists for the current (run_id, return_code) before posting to AUSTRAC or RBNZ.

MOD-038 — Data quality & reconciliation monitor

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Automated data quality and reconciliation layer for SD06. Owns the governance_meta schema — the first point of truth for whether downstream risk calculations can proceed.

What it does

MOD-038 runs as a Snowflake Task in the SD06 Task DAG, positioned immediately after the CDC refresh task and before all risk calculation modules. It executes in two stages:

Stage 1 — dbt test run (FR-225, FR-226). dbt test --select tag:mod-038 evaluates a battery of checks against every raw_cdc_* staging model: completeness (not-null), referential integrity (relationships), value range (accepted-values, custom generic tests), and format conformance. store_results: true persists one row per (run_id, dataset, check) into governance_meta.data_quality_log (append-only). A quality score per dataset is computed as passing-checks / total-checks. If any gated dataset scores below the configured threshold (default 98%, stored in governance_meta.config), the Task exits non-zero — all downstream Tasks in the DAG (MOD-032, MOD-033, MOD-035, MOD-036 etc.) do not start. This is the FR-226 halt mechanism: a Task DAG dependency, not an EventBridge event.

Stage 2 — reconciliation check (FR-227). A dbt model compares row counts in raw_cdc_core.postings against the LSN-ack metadata published by MOD-042 into the Iceberg snapshot. Discrepancies exceeding 0.01% of the aggregate are written to governance_meta.reconciliation_breaks. This is a pure Snowflake SQL operation — no Lambda, no cross-VPC Neon read.

Published views (FR-228). governance_meta.v_quality_scores and governance_meta.v_open_breaks are the published contract surfaces. Downstream modules reference these views via dbt source(). The CRO report is driven by governance_meta.daily_quality_summary (Dynamic Table, target_lag = 1 hour).

External alert (FR-226 human notification). If the Task exits non-zero, a thin Lambda publishes bank.risk-platform.data_quality_run_failed to the bank-risk-platform EventBridge bus. The sole consumer is MOD-076 (observability — alerts data engineering team). This event is a human alert, not a machine gate; the gate is enforced by the Task DAG.

Compliance rationale

REP-005 GATE is satisfied because the Task DAG dependency means no downstream regulatory return can run on data that has not passed DQ. The break cannot be hidden because data_quality_log and reconciliation_breaks are append-only with UPDATE/DELETE revoked (GOV-006 LOG). DT-004 AUTO is satisfied because the quality threshold is read from governance_meta.config — it is not hard-coded, has no override path, and is enforced at the pipeline level by dbt test failure.

Module type

Snowflake DDL + dbt + single Lambda (external alert only). No Lambda queries Snowflake. No EventBridge for intra-SD06 coordination.

Streamlit dashboard

MOD-038 ships a Streamlit page GOVERNANCE_META.STREAMLIT_DQ_SCORECARD providing: - DQ break count and break rate heat map by system domain - 30-day open-break trend per domain - Break detail list per domain (rule ID, table, column, break count, first seen) - Last-refreshed timestamp per domain

Consumed by MOD-172 (Operations & Model Intelligence Dashboard) as the DQ scorecard landing page. Cross-schema SELECT on GOVERNANCE_META.* published views required for OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
REP-005 — Data Quality & Assurance Policy GATE Source-to-report reconciliation automated — breaks cannot be hidden or ignored
DT-004 — Data Governance Policy AUTO Data quality rules enforced at pipeline level — not a manual check
GOV-006 — Internal Audit Policy LOG Internal audit has access to reconciliation break history — data quality is auditable

MOD-039 — Customer risk score model

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

XGBoost model scoring each customer's AML/financial crime risk on a 0–100 composite scale. Real-time path (60-second event-driven via Snowflake Stream + Task) and daily batch path (15-minute Dynamic Table) feed a unified v_current_scores view, with full score history maintained in an append-only table.

MOD-039 is a publisher only — it does not write directly into KYC or AML Postgres databases. Consuming domains subscribe to the bank.risk-platform/customer_risk_score_updated EventBridge event and maintain their own mirror tables:

  • bank-kyc (MOD-010)bank_kyc.party.risk_scores_mirror (see SD02 data model)
  • bank-aml (MOD-016/017)bank_aml.aml.risk_scores_mirror (see SD03 data model)

The XGBoost model ships with synthetic training data at V1. It is replaced with production data once 6 months of MOD-016/017 outcome flags accumulate via CDC (MOD-042).

Streamlit dashboard

MOD-039 ships a Streamlit page RISK_CUSTOMER.STREAMLIT_RISK_SCORE_DASHBOARD providing: - Customer risk score distribution histogram across the portfolio - Score band breakdown (low / medium / high / very high) with counts and % of portfolio - PSI vs. prior month and drift alert status - Geographic and product-type breakdowns

Consumed by MOD-171 (Risk Intelligence Dashboard) in the risk metrics overview and RAF summary. Consumed by MOD-172 (Operations & Model Intelligence Dashboard) in the model performance section. Cross-schema SELECT on RISK_CUSTOMER.* published views required for RISK_INTELLIGENCE_ROLE and OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
AML-002 — Customer Due Diligence (CDD) Policy AUTO CDD tier informed by live customer risk score — not a static assessment at onboarding
AML-005 — Transaction Monitoring Policy AUTO High risk score customers subject to enhanced monitoring automatically — no manual watchlist
DT-005 — Model Risk Management Policy LOG Risk score model in model inventory — validated against AML outcomes quarterly

MOD-040 — Churn & health score engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Logistic regression model producing churn probability and engagement health score per customer weekly. Feeds NBA engine and triggers proactive outreach.

Streamlit dashboard

MOD-040 ships a Streamlit page RISK_CUSTOMER.STREAMLIT_CHURN_DASHBOARD providing: - Churn risk distribution across the customer base - Model accuracy (AUC) and PSI from last validation run - Top 10 feature drivers of churn predictions (SHAP values) - High-churn segment deep-dive

Consumed by MOD-172 (Operations & Model Intelligence Dashboard) in the model performance section. Cross-schema SELECT on RISK_CUSTOMER.* published views required for OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
CON-001 — Customer Fairness & Conduct Policy AUTO At-risk customers proactively identified and contacted — fair conduct met before customer disengages
CON-003 — Vulnerable Customer Policy ALERT Financial stress signals in health score can trigger vulnerable customer flag — automated identification

MOD-041 — Categorisation & merchant enrichment model

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

XGBoost multi-class classifier. Retrained weekly on customer correction signals. Confidence-routed — ≥0.85 auto, 0.60–0.84 prompt, <0.60 show Other. See ADR-017.

Build notes — 2026-05-14

The AWS SCP blocker on the bank-merchant-assets-{env} S3 bucket is resolved (bank-platform commit 911a11f7 provisions the bucket). The current deploy failure is unrelated to S3: the Python UDF NORMALISE_MERCHANT is not deployed in dev because the CI pipeline runs with the HAS_DCM=false + HAS_DBT_PROJECT=false workaround, which skips the Snowpark deploy step. The dbt model int_normalised_merchant then fails with Unknown user-defined function BANK_DEV_RISK.RISK_CUSTOMER.NORMALISE_MERCHANT.

Resolution options (pick one): - Deploy the UDF manually once (pnpm udf:deploy in the module directory); it persists in Snowflake and subsequent pipeline runs will find it. - Make the UDF deploy step unconditional in bank-platform/.gitlab/ci/templates/risk-platform.gitlab-ci.yml so it runs regardless of HAS_DCM / HAS_DBT_PROJECT. This is the cleanest long-term fix. - Resolve the DCM ownership privilege issue (see MOD-056 notes), flip HAS_DCM=true + HAS_DBT_PROJECT=true, and the UDF deploy runs automatically.

Streamlit dashboard

MOD-041 ships a Streamlit page RISK_CUSTOMER.STREAMLIT_CATEGORISATION_DASHBOARD providing: - Transaction categorisation coverage rate (% of transactions with non-null category, by category tree level) - Merchant-enrichment match rate and top unmatched merchant patterns - Model accuracy on held-out validation set (macro F1, per-category breakdown) - Category volume trends over 30 days

Consumed by MOD-172 (Operations & Model Intelligence Dashboard) in the model performance section. Cross-schema SELECT on RISK_CUSTOMER.* published views required for OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy AUTO Transaction descriptions and categories are accurate and meaningful — not raw acquirer strings
DT-005 — Model Risk Management Policy LOG Categorisation model versioned, retrained on feedback, and performance-monitored

MOD-055 — Onboarding fraud scoring engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Real-time fraud scoring engine applied to every onboarding application. Evaluates device fingerprint signals, velocity patterns, duplicate identity attribute clusters, and synthetic identity indicators to produce a fraud score and a BLOCK / REVIEW / ALLOW disposition that gates the onboarding decision orchestrator.

Produces explainable signal contributions aligned to the signal taxonomy (IDENTITY, DEVICE, BEHAVIOUR, NETWORK) so that fraud outcomes can be reviewed, challenged, and used to retrain models.

Not to be confused with MOD-023 (transaction fraud scorer), which operates post-onboarding on payment events.

Policies satisfied:

Policy Mode Description
AML-013 — Onboarding Fraud & Identity Integrity Policy GATE Device, velocity, duplicate, and synthetic identity signals evaluated at the point of application; BLOCK / REVIEW / ALLOW outcome gates the onboarding decision orchestrator.

MOD-056 — Compliance visibility engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Purpose

Visualise how the bank's platform satisfies its regulatory obligations. The bank-wiki is the authoritative obligation register — it contains every regulation the bank is subject to, every compliance policy derived from those regulations, and (in each module's policies_satisfied array) exactly how the platform satisfies each policy in code. MOD-056 imports that obligation chain into Snowflake and surfaces it as a live compliance dashboard, from regulation → policy → module → runtime evidence.

What it does

The module owns the REGULATORY Snowflake schema and operates in two layers:

Layer 1 — Design-time evidence. A wiki-import Lambda runs on a daily schedule and fetches the bank-wiki's compiled AI context pages (policies, systems, index). It parses and upserts the full set of regulations, policies, policy-regulation links, modules, and policies_satisfied entries into the WIKI_* tables. These tables are the structural proof that each policy is satisfied: they record which module satisfies which policy, the satisfaction mode (AUTO / GATE / LOG / ALERT / CALC), and the description of the mechanism. For every Built or Deployed module, this constitutes complete architectural compliance evidence. No manual curation is needed — the wiki IS the sign-off.

Layer 2 — Runtime evidence. A Snowflake Task (TASK_AUTO_EVIDENCE_LINKER) runs hourly and reads sibling SD06 module views via source() declarations, inserting rows into COMPLIANCE_EVIDENCE when a runtime artefact confirms an obligation was fulfilled for a given period. v1 wires MOD-080's V_PERIOD_CLOSE_METRICS — every period where succeeded_count > 0 produces one evidence row for the statutory reporting obligation. v2 extends to MOD-058 (breach notifications), MOD-060 (FATCA/CRS), and MOD-036 (prudential returns) as those modules ship.

Streamlit dashboards are the primary output. The bank uses these to answer: which policies does this platform satisfy? Which modules satisfy them? Is each module built and deployed? How many runtime evidence records exist in the last 12 months? Which policies have no satisfying module (NFR-011 zero-tolerance check)? The dashboards are filterable by risk domain, jurisdiction, and policy domain.

A deadline-alerter Lambda runs at 09:00 daily, queries time-bound obligations in WIKI_POLICIES (annual/periodic regulatory submissions, licensing renewals), and publishes bank.regulatory.obligation_deadline_approaching events for obligations within the configured alert window (default 90 days) with no recent evidence record.

Compliance reason

REP-006 requires the CCO to maintain a current regulatory obligation register and present compliance status to the Board Risk Committee quarterly. This module provides the governed Snowflake-native system of record and automates the alert and reporting cadence. The wiki-sourced seed means the obligation register is always consistent with the CCO-maintained policy wiki — no separate governance review cycle needed.

Commercial reason

Supervisory risk materialises when a bank cannot demonstrate, on demand, how its platform satisfies its regulatory obligations. Governance By Design — the platform's founding architectural principle — means compliance is built into every module's code. MOD-056 makes that built-in compliance visible and auditable. It is the answer to the regulator's question: "Show me your controls."

Design notes

Bank-wiki as seed: The wiki's AI context pages (https://bank-wiki.pages.dev/ ai-context/policies/ and /systems/) are structured Markdown published by the wiki CI pipeline on every compile. The import Lambda fetches these pages, parses policy entities and policies_satisfied arrays, and upserts into the WIKI_* tables. The idempotency key is (policy_code, module_id) for satisfaction rows; policy_code for policy rows; module_id for module rows.

No external regulator feeds: RBNZ/APRA/FMA/AUSTRAC RSS polling is explicitly out of scope. When regulations change, the CCO team updates the wiki. The wiki update propagates into MOD-056 on the next daily sync. The wiki is the single source of truth for regulatory obligation; maintaining a second register in parallel would create divergence risk.

No CSV exports for auditors: All output is Snowflake Streamlit. If the bank needs a point-in-time file for an audit, they export from Snowsight through their own governance process — that is the bank's responsibility, not this module's.

ADR-046 compliance: The auto-evidence linker is a Snowflake Task (not a Lambda opening a Snowflake connection). Cross-module reads use source() declarations. Single-schema ownership — no other module writes to the REGULATORY schema.

Build notes — 2026-05-14

Deploy fails with:

003001 (42501): SQL access control error:
Insufficient privileges to operate on schema 'REGULATORY'.

The REGULATORY schema was pre-created by BANK_NONPROD_RISK_ROLE before CI took over ownership. The CI service account (SF_ROLE) cannot create views in it. Under the HAS_DCM=false workaround, the DCM ownership-transfer step that would grant SF_ROLE the necessary privileges is skipped.

Resolution: Grant BANK_NONPROD_RISK_ROLE to SF_USER in Snowflake so the CI service account can take ownership of the DCM project and its schemas. This is the long-term fix documented in the GitLab CI handover page. Once granted, flip HAS_DCM=true + HAS_DBT_PROJECT=true in the pipeline and remove the workaround. Stopgap alternative: one-off GRANT CREATE VIEW ON SCHEMA REGULATORY TO ROLE SF_ROLE.

MOD-056's Streamlit dashboards (compliance map, policy satisfaction matrix, obligation deadline tracker) are accessible via a navigation link from MOD-172 (Operations & Model Intelligence Dashboard) per FR-818. MOD-056 does not depend on MOD-172 — the link is one-directional. MOD-172 depends on MOD-056 being deployed for the link to resolve.

Policies satisfied:

Policy Mode Description
REP-006 — Regulatory Change Management Policy ALERT Deadline-alerter emits bank.regulatory.obligation_deadline_approaching for time-bound obligations approaching their due date; ALERT_OBLIGATION_DEADLINE_APPROACHING DCM alert routes to MOD-076 SNS for CCO notification.
GOV-006 — Internal Audit Policy LOG WIKI_IMPORT_LOG and OBLIGATION_EVENTS are append-only audit tables recording every wiki-sync run and every obligation state transition with timestamp and run_id — immutable per NFR-024.

MOD-057 — Statistical returns & survey engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Approval gate (FR-807)

This module does not submit automatically. The submission orchestrator checks REGULATORY.RETURN_APPROVALS before posting to RBNZ or APRA. If no approval record exists for the current (run_id, return_code), the orchestrator aborts and alerts the Finance team. The approval is written by MOD-170 (Regulatory Submissions Portal) — a Finance officer reviews the assembled return in the portal Streamlit and clicks Approve. No code path in this module can bypass the gate.

Purpose

Automate the preparation and submission of statistical and survey returns to RBNZ and APRA from the governed data pipeline, replacing manual spreadsheet-based submissions.

What it does

The module connects to the governed regulatory reporting pipeline and extracts the data required for each statistical return obligation — RBNZ Statistical Returns and APRA EFS collections — on the schedule defined in the reporting obligation register.

For each return, the module performs pre-submission validation: checking completeness, applying regulator-specified business rules, and reconciling submission totals to internal management accounts. Returns that fail validation are quarantined and the responsible officer is alerted.

Approved returns are submitted electronically to the relevant regulator via the prescribed submission channel (RBNZ data portal and APRA RAAP). Submission confirmation receipts are retained and cross-referenced against the obligation register to confirm on-time lodgement.

The module maintains a register of all statistical reporting obligations with next due dates, enabling the CCO and CFO to confirm the annual schedule sign-off required by REP-008.

Compliance reason

REP-008 requires statistical returns to be sourced from the governed pipeline and submitted on time to RBNZ and APRA. Manual submission processes are error-prone and lack the reconciliation and on-time tracking controls required by the policy.

Commercial reason

Automated statistical return preparation reduces the manual effort in the Finance and Risk functions and eliminates the risk of late submission penalties and regulatory findings arising from manual process failures.

Policies satisfied:

Policy Mode Description
REP-008 — Statistical & survey reporting AUTO Automates the preparation and submission of RBNZ and APRA statistical returns from the governed data pipeline.
REP-005 — Data Quality & Assurance Policy GATE Submission is gated by Finance officer approval in REGULATORY.RETURN_APPROVALS (via MOD-170); the submission orchestrator aborts if no approval record exists for the current (run_id, return_code) before posting to RBNZ or APRA.

MOD-058 — Regulatory incident & breach notification engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

Manage the regulatory incident notification workflow, ensuring that material operational and security incidents are classified, recorded, and notified to RBNZ, APRA, FMA, and other relevant regulators within required timeframes.

What it does

The module receives incident records from the operational resilience monitor (MOD-042) and the security operations function. It applies the platform's incident classification matrix to determine whether an incident meets the notification thresholds under CPS 230 (operational incidents), CPS 234 (information security incidents), and applicable NZ supervisory frameworks.

For each notifiable incident, the module generates a notification record pre-populated with the required fields: incident description, systems and customers affected, cause, and remediation steps. The notification is routed to the CCO and CTO for review and approval before submission. Once approved, the module submits the notification to the relevant regulator via the prescribed channel and records the submission timestamp and acknowledgement receipt.

The module tracks open incidents through to resolution and prompts for post-incident review completion within 30 days. An annual notification capability test is scheduled and tracked by the module.

Compliance reason

REP-009 imposes 72-hour notification deadlines under CPS 230 and CPS 234 and equivalent NZ requirements. Without an automated workflow, the platform risks missing regulatory notification deadlines in high-pressure incident situations.

Commercial reason

Early and accurate regulator notification reduces the risk of supervisory escalation and demonstrates operational maturity. The module also provides the Board with a complete view of incident notification performance.

Policies satisfied:

Policy Mode Description
REP-009 — Regulatory incident & breach notification AUTO Manages the incident register, routes notifications to the correct regulator within required timeframes, and tracks acknowledgement receipts.

MOD-060 — FATCA/CRS/AEOI reporting engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Approval gate (FR-807)

This module does not submit automatically. The submission orchestrator checks REGULATORY.RETURN_APPROVALS before transmitting to IRD or ATO. If no approval record exists for the current (run_id, return_code), the orchestrator aborts and alerts the Finance team. The approval is written by MOD-170 (Regulatory Submissions Portal) — a Finance officer reviews the assembled return in the portal Streamlit and clicks Approve. No code path in this module can bypass the gate.

Purpose

Automate FATCA and CRS due diligence workflows, account classification, and the annual FATCA/CRS/AEOI report preparation and submission to IRD and ATO.

What it does

The module manages the FATCA and CRS customer due diligence lifecycle: triggering self-certification requests at account opening, tracking certification receipt and validity, and applying the default classification rules where certification is not received.

Account classifications (US person, CRS reportable, non-reportable) are maintained in the module's classification register and updated automatically when new certifications are received or when account holder information changes in a way that affects classification.

Annually, the module extracts reportable account data from the classification register and the governed data pipeline, generates the FATCA XML and CRS XML report files in the formats required by IRD and ATO, and submits them via the prescribed e-filing channels. Submission confirmation receipts are retained.

The module provides the CCO with an annual FATCA/CRS compliance dashboard covering: accounts reviewed, classifications by type, and report submission status.

Compliance reason

REP-011 requires annual FATCA and CRS reports to be submitted to IRD and ATO and the underlying due diligence to be documented and retained. Manual management of self-certification and report generation creates compliance risk at the scale of the platform's customer base.

Commercial reason

Automated FATCA/CRS management reduces the tax reporting compliance burden on the Finance and Operations functions and eliminates the risk of late filing penalties and information exchange failures.

Policies satisfied:

Policy Mode Description
REP-011 — Tax & information reporting (FATCA/CRS/AEOI) AUTO Automates FATCA and CRS due diligence workflows, account classification, and annual report preparation and submission to IRD and ATO.
PRI-004 — FATCA & CRS Compliance Policy LOG Customer financial data disclosed to overseas tax authorities (IRD/ATO) under FATCA/CRS is logged — every cross-border data transmission is recorded with recipient, data scope, legal basis, and transmission timestamp.
REP-005 — Data Quality & Assurance Policy GATE Submission is gated by Finance officer approval in REGULATORY.RETURN_APPROVALS (via MOD-170); the submission orchestrator aborts if no approval record exists for the current (run_id, return_code) before transmitting to IRD or ATO.

MOD-080 — Statutory financial reporting & ERP integration

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

The statutory financial reporting and ERP integration module produces the bank's statutory accounts and regulatory financial statements from Snowflake, and pushes a structured GL feed to the bank's ERP on the statutory schedule.

Purpose

The bank has two distinct financial reporting obligations: management accounts (internal, continuous) and statutory accounts (external, regulated, filed with the Companies Office and submitted to RBNZ/APRA). This module serves the statutory side — it takes the Snowflake analytical layer as its source, produces audited-format outputs, and delivers them to the ERP for filing and consolidation.

What it does

  • Trial balance extraction — extracts a complete trial balance from the Snowflake CORE database on the statutory schedule (monthly for management, quarterly for regulatory, annually for statutory filing)
  • Statement production — produces P&L, balance sheet, and cash flow statement in the statutory format required for NZ Companies Act / AU Corporations Act filings
  • ERP GL feed — formats the journal entries as a structured feed (CSV or API push, depending on the ERP) and delivers them to the bank's general ledger system; the ERP is the system of record for filing, not Snowflake
  • Reconciliation gate — before each extract, the module confirms that the Snowflake trial balance agrees with Postgres account balances; any variance blocks the extract and raises an operations alert
  • RBNZ/APRA prudential feeds — works alongside MOD-036 (Prudential return builder) to produce the financial data component of prudential returns; MOD-036 owns the regulatory submission; this module owns the GL-quality financial data it depends on

What it does not do

This module does not produce risk-weighted capital calculations (MOD-033), liquidity ratios (MOD-032), or AML regulatory reports (MOD-037). It does not write to Postgres — it reads from Snowflake and pushes to the ERP.

Build notes — 2026-05-14

Two distinct failures:

1. STATUTORY schema privileges — same DCM ownership root cause as MOD-056. Under HAS_DCM=false, DCM-declared tables (REPORT_RUNS, ERP_PUSH_LOG, RECONCILIATION_RUNS, EB_PUBLISH_CURSOR) are never created in the STATUTORY schema. The dbt view v_period_close_metrics joins to REPORT_RUNS and fails compilation. Resolution: the same BANK_NONPROD_RISK_ROLE grant that unblocks MOD-056 also unblocks this failure path in MOD-080.

2. Dynamic Table lag inversion (code bug)reconciliation_status_current has target_lag='5 minutes' while its dependency trial_balance_period has target_lag='15 minutes'. Snowflake requires a downstream Dynamic Table's lag to be ≥ the largest lag of its dependencies.

002715 (22023): Dynamic Table 'BANK_DEV_RISK.STATUTORY.RECONCILIATION_STATUS_CURRENT'
has a lag of 5 minutes, less than the largest lag of its dependencies.

Fix: In dbt/models/MOD-080-statutory-reporting/reconciliation_status_current.sql, change target_lag='5 minutes' to target_lag='15 minutes' (or '1 hour' to align with the hourly reconciliation Lambda cadence). One-line change.

Streamlit dashboard

MOD-080 ships a Streamlit page STATUTORY.STREAMLIT_FINANCIALS_VIEWER providing: - Period profit and loss statement (current and prior period side-by-side) - Balance sheet (assets, liabilities, equity) - Cash flow statement - Period navigation (prior 8 quarters accessible) - Each line item links to the trial balance rows that compose it via model_run_id

Consumed by MOD-172 (Operations & Model Intelligence Dashboard) as the statutory financials viewer section. Cross-schema SELECT on STATUTORY.* published views required for OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
REP-004 — Financial Statements Policy AUTO Statutory financial statements (P&L, balance sheet, cash flow) produced from the Snowflake analytical layer on the regulatory schedule — no manual extraction.
REP-001 — Regulatory Reporting Policy AUTO Regulatory reporting feeds drawn from the same Snowflake data as internal management accounts — single source of truth for all statutory obligations.
GOV-006 — Internal Audit Policy LOG All ERP journal entries and statutory extracts are logged with the source data lineage — auditors can trace every figure to its transaction origin.

MOD-085 — Market rates ingestion & normalisation

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-085 is the single point of ingestion for all external market reference data consumed by the bank's analytical, risk, and operational systems. It owns the normalisation layer that makes consumers provider-agnostic, and the write-back path that makes Snowflake-sourced FX spot available to Postgres-based operational modules.

What it does

Snowflake Marketplace normalisation. The module subscribes to one or more Marketplace provider data shares (FX spot, FX forward curves, swap/OIS curves, benchmark rates). Snowflake Dynamic Tables in the market.* schema translate provider-specific column names, decimal conventions, and timestamp formats into a canonical form. Downstream consumers read only from market.* — they are never coupled to the provider schema.

The canonical market.* tables produced are:

Table Content Typical refresh
market.fx_spot_current Latest mid-rate per currency pair 5–15 min (provider)
market.fx_forward_curve Tenor points ON/TN/1W/1M/3M/6M/1Y per pair EOD
market.swap_curve AUD and NZD par swap rates 3M–10Y EOD
market.ois_curve SOFR, SONIA, BBSW, BKBM overnight index swap rates EOD
market.benchmark_rates RBA OCR, RBNZ OCR, BBSW, BKBM daily fixes Daily

BKBM direct feed. A scheduled Lambda runs on each New Zealand business day to fetch BKBM from the NZFMA HTTPS endpoint. The rate is loaded into market.benchmark_rates. If the feed is unavailable, the previous business day's rate is carried forward with a data quality flag; an alert fires if the carry-forward persists for more than one day.

Operational FX write-back. After each market.fx_spot_current refresh, a write-back Lambda upserts the latest spot mid-rate for all supported currency pairs into payments.fx_rates in the SD04 Postgres database. On successful upsert, a bank-platform.market_rates_updated EventBridge event is emitted. SD04 operational modules (MOD-025 rate lock, MOD-071 payment validation) consume from Postgres, not Snowflake, ensuring the payment path is never inline on Snowflake query latency.

Provider abstraction

Provider selection is a procurement decision with no architectural consequence. The normalisation Dynamic Tables are the only code that references provider column names. Swapping or supplementing a provider requires updating those tables only — no consumer changes. See ADR-039 for the rationale and the BKBM exception.

Compliance context

All ingestion events are logged with provider identifier, data version, and ingestion timestamp. Any market rate used in a regulatory calculation (LCR, NSFR, ECL, ILAAP stress test, prudential return) is traceable back to its source version via this log. This satisfies audit requirements for REP-005 and supports ILAAP data lineage obligations.

Streamlit dashboard

MOD-085 ships a Streamlit page MARKET.STREAMLIT_RATES_DASHBOARD providing: - Current market rates by currency pair (NZD/USD, AUD/USD, NZD/AUD) and tenor (ON, 1W, 1M, 3M, 6M, 1Y) - Rate history charts over 30 and 90 days - SOFR, BKBM, BBSW rate term structure - Data freshness indicator (last ingestion timestamp per source)

Consumed by MOD-171 (Risk Intelligence Dashboard) as market rate context in the IRRBB sensitivity page. Cross-schema SELECT on MARKET.* published views required for RISK_INTELLIGENCE_ROLE.

Policies satisfied:

Policy Mode Description
REP-005 — Data Quality & Assurance Policy LOG All market data ingestion events are logged with provider, version, and timestamp — full audit trail for any rate used in regulatory calculations or product pricing.
CLQ-002 — Liquidity Risk Management Policy CALC Swap and OIS curves sourced by this module are inputs to the LCR and NSFR Dynamic Tables — data quality failures block liquidity ratio computation.

MOD-086 — Funds transfer pricing engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-086 implements the bank's funds transfer pricing (FTP) framework in Snowflake. It converts market swap and OIS curves into a daily internal rate grid that every product line uses as the cost or benefit of holding a funding position. Without FTP, product P&L is meaningless — a mortgage book cannot be assessed as profitable or loss-making unless it is charged a fair cost of the term funding it consumes.

What it does

Daily TP rate grid. After end-of-day curve publication by MOD-085, the FTP engine reads market.swap_curve and market.ois_curve. It applies a configurable liquidity premium overlay (set and maintained by the Treasury function) to produce a TP rate for each of nine standard tenor buckets: ON, 1M, 3M, 6M, 1Y, 2Y, 3Y, 5Y, and 10Y. The grid is written to the ftp.transfer_prices Snowflake Dynamic Table.

TP rate write-back. A write-back Lambda reads the computed TP grid from Snowflake and upserts it to the treasury.tp_rates table in the SD01 Postgres database. SD05 (credit decisioning) and SD01 (product rate configuration) read TP rates from Postgres — they are never inline on Snowflake during a customer interaction.

NIM attribution. The engine joins the TP grid to every active loan and deposit balance in the bank's Snowflake replica. For each balance it calculates the TP cost (for loans: the funding cost attributed to treasury) or TP benefit (for deposits: the funding credit passed to treasury). Net interest margin is then attributed by product code, business line, and jurisdiction. Results are published daily to ftp.nim_attribution. This is the primary source for management accounts and product profitability reporting.

Audit trail. Every version of the TP rate grid is retained in full with its effective date, the curve source version identifier from market.*, and the liquidity premium basis points applied. A minimum of seven years of history is maintained, enabling reconstruction of the TP rate that applied to any loan or deposit on any historical business day.

Relationship to product pricing

The TP grid does not set customer-facing interest rates. It sets the internal cost-of-funds baseline. Product managers in SD01 configure margin bands (LVR tier × credit tier → margin over TP) on top of the TP base rate to determine the published lending rate. Similarly, deposit rates are set as TP benefit less the margin retained by the bank. FTP makes the relationship between market rates, treasury cost, and product pricing explicit and auditable.

Liquidity premium

The liquidity premium overlay is the bank's assessment of the cost of holding a liquidity buffer appropriate to each tenor bucket. It is not derived from market data — it is a Treasury policy decision reviewed quarterly. The premium is stored in a configuration table in Snowflake and versioned; the FTP engine uses the premium version active on each business day.

Streamlit dashboard

MOD-086 ships a Streamlit page FTP.STREAMLIT_FTP_DASHBOARD providing: - Current FTP rate table by product type and tenor (mortgage, personal loan, term deposit, at-call savings — each with NZD and AUD rates) - FTP rate history over 90 days - Marginal cost of funds vs. FTP rate spread by product - NII contribution estimated from FTP rates

Consumed by MOD-171 (Risk Intelligence Dashboard) in the risk metrics overview section. Cross-schema SELECT on FTP.* published views required for RISK_INTELLIGENCE_ROLE.

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy CALC NIM attribution by business line is produced daily from TP-adjusted loan and deposit balances — management accounts reflect the true cost and benefit of funds for each product segment.
CLQ-003 — Capital Planning & Stress Testing Policy CALC The TP rate grid encodes the liquidity premium charged to each tenor bucket — the FTP engine ensures every product price embeds the cost of holding that liquidity position.

MOD-088 — Expense classification engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-088 is the expense classification engine. It takes enriched transaction events from MOD-087 and produces a multi-dimensional classification for each transaction:

Dimension Values
Ownership Personal / Business / Mixed / Property
Purpose Client meeting / Supplies / Commute / Travel / etc.
Tax treatment Fully claimable / Partially claimable / Non-claimable
Accounting mapping Chart of accounts code
Confidence 0–100%
Classification basis Merchant / Behavioural / Geo / Rule / User-confirmed

Classification inputs

The engine combines multiple signal types:

  • Merchant intelligence — normalised merchant name, MCC, prior classification of this merchant in the user's history, population-level signal (privacy-safe aggregates)
  • Behavioural patterns — time of day, day of week, recurrence (weekly subscriptions = likely business software), spend amount vs baseline, spend sequences (flight + hotel + meals = business trip)
  • Geo-spatial context — home cluster, work cluster, travel period signals from MOD-089
  • User-confirmed rules — explicit and implicit overrides from MOD-090
  • Xero/MYOB history — imported on onboarding to bootstrap the model

Model approach

The classification model is a supervised ML model trained on labelled transaction histories, with a rule overlay for high-confidence cases (e.g. transactions at known payroll providers are always employer-sourced income). The model is retrained periodically from user-confirmed classifications across the anonymised portfolio.

Design phase

This module is in design. Build begins in Phase 2 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Classification signals and model inputs are logged to support data minimisation review and individual access requests under PRI-001.

MOD-089 — Geo-spatial processor

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-089 is the geo-spatial processor. It maintains location clusters for each customer and uses those clusters to provide spatiotemporal classification signals to MOD-088.

Location clusters

For each customer, MOD-089 maintains:

  • Home cluster — the geographic centroid and radius of transactions occurring near the customer's registered residential address
  • Work cluster — transactions near the registered business address, or the dominant weekday-daytime location cluster if no business address is registered
  • Travel clusters — sustained transaction sequences outside both home and work clusters, automatically detected as travel periods

Trip detection

A travel period is detected when: 1. A flight (or a departure from the home city for more than one day) is inferred from transactions 2. Local transactions cluster in a new geography for a sustained period 3. Return to the home geography closes the trip

Detected trips are used by MOD-088 to classify dining, transport, and accommodation as travel expenses, which carry different personal/business and tax treatment signals than equivalent spending at home.

Geo signal → classification

Location context Time context Signal emitted
Within work cluster Business hours weekday Strong business
Within home cluster Evening / weekend Strong personal
Outside both clusters Sustained sequence Travel period
Recurring geo sequence Similar duration to prior trips Recurring business travel

Design phase

This module is in design. Build begins in Phase 2 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Location cluster data is subject to data minimisation policy; processing basis and retention period are recorded per PRI-001.

MOD-092 — Tax logic engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-092 is the tax logic engine. It applies NZ and AU GST rules, income tax rules, and expense deductibility rules to the classified transaction portfolio, producing real-time tax position summaries and period-end filing-ready outputs.

NZ GST

For sole traders and SMEs registered for GST, MOD-092 accumulates: - Output tax: GST collected on taxable sales (relevant for businesses with invoicing) - Input tax credits: GST claimable on business expenses

The engine produces a quarterly GST return summary — gross sales, total purchases, GST on sales, GST on purchases, and net GST payable or refundable — ready for customer review and (in Phase 4) direct IRD lodgement via the myIR API.

AU GST / BAS

For AU entities, MOD-092 produces the Business Activity Statement inputs: G1 (total sales), G11 (non-capital purchases for creditable purposes), and the net GST position. Phase 4 target: direct ATO SBR2 lodgement.

Expense deductibility

Each classified transaction receives a deductibility flag from MOD-088. MOD-092 applies the tax rules that determine whether the classification translates to a deductible expense: - Business meals: 50% deductible (entertainment rules) - Home office: apportioned by use - Vehicle: logbook method or kilometre rate - Commute: not deductible

For property expenses, deductibility is assessed in conjunction with MOD-094 and MOD-095 (property attribution and ring-fencing).

Provisional tax estimation (NZ)

For sole traders, MOD-092 maintains a rolling provisional tax estimate based on year-to-date classified income and deductible expenses. This is surfaced in the app as a running figure, updated after each classified transaction.

Design phase

This module is in design. Build begins in Phase 3 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Tax position calculations and deductibility determinations reference personal financial data; all computation inputs and outputs are logged for data minimisation audits.

MOD-094 — Property attribution engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-094 is the property attribution engine. It treats each rental property as a virtual operating context — an economically meaningful unit with its own income ledger, expense ledger, and tax profile — without requiring a separate legal entity.

Property setup

A property is onboarded to the platform by the customer identifying an address and the ownership structure (personal, joint, company, trust). MOD-094 then: 1. Detects regular inbound payments matching a rental income pattern and prompts the customer to confirm tenant attribution 2. Creates the property operating context in the party graph (via MOD-096 where multiple ownership entities are involved) 3. Begins attributing subsequent transactions to the property based on explicit rules and MOD-088 classification signals

Expense attribution

Expenses are attributed to a property by: - Explicit merchant rules (e.g. "Harcourts property management is always 12 Smith St") - Address-derived geo signals (repairs and maintenance near the property address) - Customer confirmation on first encounter and implicit rule learning thereafter

Capital vs revenue flagging

Large repair or improvement transactions trigger a customer prompt: "Is this a repair (deductible now) or an improvement (capital expenditure — depreciable)?" This is a common source of IRD audit interest. The module stores the customer's determination with a timestamp for audit.

Property P&L

MOD-094 maintains a running P&L per property per period: rental income received, operating expenses, financing costs (mortgage interest), and the net result before ring-fencing. This feeds MOD-095 (ring-fencing logic engine) and MOD-093 (accounting mapper for Xero output).

Design phase

This module is in design. Build begins in Phase 3 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Property P&L data and rental attribution records constitute personal financial data; all processing and access events are logged for data minimisation audits.

MOD-095 — Ring-fencing logic engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-095 is the ring-fencing logic engine. It applies the NZ residential rental ring-fencing rules (Income Tax Act 2007, ss EE 1–8) to the property portfolio managed by MOD-094, maintaining the ring-fenced loss carry-forward register and computing the portfolio-level profit offset each period.

Ring-fencing mechanics

Under NZ residential ring-fencing rules, rental losses from residential properties cannot offset other income (salary, business income). MOD-095 enforces this by:

  1. Receiving property-level net income/loss from MOD-094 at period end
  2. For profitable properties: recording the profit as normal income
  3. For loss-making properties: ring-fencing the loss into a carry-forward register; it is not available to offset other income
  4. Applying portfolio-level offset: ring-fenced losses from Property A can offset profits from Property B in the same portfolio
  5. Testing for the portfolio profitability exemption: if the combined portfolio is profitable, ring-fencing rules do not apply

Exemptions handled

  • Portfolio profitability test — if total rental portfolio income exceeds total rental losses, the ring-fencing rules do not apply for that year
  • Land business exemption — developers and dealers operating as a land business are not subject to residential ring-fencing; the module flags these cases for manual review
  • Mixed-use holiday homes — MOD-095 handles the bright-line/mixed-use apportionment rules for properties that are both personally used and rented

Ring-fenced loss register

Each ring-fenced loss is stored with the tax year, property, and amount. Carry-forwards are applied to future rental profits in chronological order (oldest losses first). The register is the output used by MOD-093 for tax summary outputs and accountant exports.

Design phase

This module is in design. Build begins in Phase 3 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Ring-fenced loss carry-forward registers and property tax positions constitute personal financial data; computation inputs and register writes are logged for data minimisation audits.

MOD-098 — Cost attribution engine

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-098 converts raw usage events and infrastructure costs into attributed financial figures — per licensee, per module, per billing period. It is the engine that makes the three-part tariff (customer levy + facility fee + variable consumption) computable in Snowflake, and produces the data that the billing report (MOD-099) and internal finance reporting (MOD-080) consume.

Cost sources

1. Usage events (from MOD-097)

Reads metering.usage_events in Snowflake. Joins to metering.cost_rates to derive attributed cost per event type. Aggregates to daily tenant × module summaries.

2. AWS infrastructure costs

A daily scheduled Lambda calls the AWS Cost Explorer API (get_cost_and_usage) filtered by the tenant_id tag. Results are written to metering.aws_cost_daily. The Cost Explorer data is 24–48 hours lagged — the current day's infrastructure cost is estimated from the prior day's rate until actuals arrive.

Dimensions pulled: - SERVICE (Lambda, S3, Kinesis, API Gateway, DynamoDB, EventBridge, Secrets Manager, etc.) - UsageType (for resource-level granularity within each service) - Tag: tenant_id, module_id

Untagged costs accumulate to a tenant_id = "unattributed" bucket. These are monitored and alerted via MOD-034 if they exceed a configured threshold of total daily AWS spend.

3. Snowflake compute costs

Reads snowflake.account_usage.query_history and snowflake.account_usage.metering_history (1-hour lag). For dedicated per-tenant warehouses, credits are attributed directly. For shared warehouses, attribution is proportional:

tenant_credit_cost = (tenant_query_credits / total_shared_warehouse_credits) × warehouse_cost_for_period

Results written to metering.snowflake_credit_daily.

4. External API costs (market data, enrichment, NZFMA)

Pulled by MOD-097's external cost Lambda and available in metering.external_api_costs. Attribution is by the module that made the call — which is tagged in the usage event.

Billing model computation

Customer levy

customer_levy = active_customers_this_month × rate_card.customer_per_month
active_customers_this_month is the count of distinct customer_id records with at least one transaction in the billing period, read from CDC-replicated accounts.accounts.

Facility fee

facility_fee = SUM(activated_modules × rate_card.module_per_month)
Activated modules are defined in billing.tenant_modules (set at onboarding; updated when licensee activates/deactivates a module).

Variable consumption

For each variable resource type (Snowflake credits, API calls, ML inferences, notification sends, storage-GB):

variable_charge = MAX(0, actual_usage − included_threshold) × rate_card.variable_rate
Included thresholds are defined in billing.tenant_tiers (the tier the licensee subscribed to).

Infrastructure passthrough (optional)

infra_passthrough = actual_aws_cost_tagged_to_tenant + actual_snowflake_cost_attributed_to_tenant
This is the cost-basis transparency line. Shown separately on the report so licensees can see the markup.

Rate card management

metering.cost_rates is the canonical rate card. It is versioned by effective_from date. A rate card change (e.g. AWS price decrease, Snowflake pricing update) creates a new version; existing billing periods use the rate card active at their start date. Rate card updates require approval and are never retroactive.

Output Dynamic Tables

Table Refresh Description
metering.daily_tenant_summary INCREMENTAL, 1h lag Per-tenant daily cost by module and resource type
metering.billing_period_summary FULL, 4h lag Rolling billing period totals: levy + facility + variable + passthrough
metering.unit_economics FULL, daily Internal view: cost to serve per module per customer, gross margin by tenant
metering.unattributed_costs INCREMENTAL, 1h lag AWS costs with no tenant tag — monitored for tagging gaps

Streamlit dashboard

MOD-098 ships a Streamlit page METERING.STREAMLIT_COST_DASHBOARD providing: - Cost allocation by system domain (SD01–SD08) for the current month and trailing 12 months - Cost allocation by product line - Period-over-period comparison - Cost driver category breakdown per domain (infrastructure, headcount-equivalent, shared services)

Consumed by MOD-171 (Risk Intelligence Dashboard) in the risk metrics overview section (cost attribution panel). Consumed by MOD-172 (Operations & Model Intelligence Dashboard) in the cost attribution view. Cross-schema SELECT on METERING.* published views required for RISK_INTELLIGENCE_ROLE and OPERATIONS_ROLE.

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy CALC Produces daily attributed cost and running billing period totals per licensee — the authoritative source for SaaS invoices and internal gross margin reporting.

MOD-101 — Wealth intelligence engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

Computes a daily net worth snapshot per customer by combining internal bank data (account balances, loan positions, property equity) with external asset data (KiwiSaver/superannuation from MOD-100). Produces KiwiSaver health indicators — government member tax credit utilisation gap and PIR mismatch risk — as customer-facing insights.

This module does not provide financial advice. It computes factual indicators from observable data and surfaces them in the app with appropriate framing (e.g. "You may be missing out on $X of government contributions — contributions of $Y more this year would unlock the full match").

Net worth computation

Daily, after MOD-100 external asset refresh completes:

Net worth =
    Deposit account balances (SD01 accounts where account_type IN ('TRANSACTION','SAVINGS','TERM'))
  + External asset balances  (SD01 assets where asset_type IN ('KIWISAVER','SUPERANNUATION'))
  - Loan balances outstanding (SD01 accounts where account_type IN ('HOME_LOAN','PERSONAL_LOAN','OVERDRAFT'))
  + Property equity estimate  (properties.estimated_value - SUM of secured loan balances)

Results published to wealth.net_worth_daily Snowflake Dynamic Table (1-hour lag from source refresh).

Liquidity tiers

Net worth is stratified into four tiers for the app dashboard:

Tier Asset types Accessible
Instant access Transaction + savings accounts Immediately
Short-term locked Term deposits, notice accounts On maturity / notice period
Illiquid Property equity On sale (weeks–months)
Retirement-locked KiwiSaver / AU superannuation On eligibility (age 65 NZ; preservation age AU)

KiwiSaver health indicators

Government member tax credit gap (NZ)

The NZ government contributes $0.50 per $1 of member contribution, up to a maximum of $521.43/year. To receive the full credit, a member must contribute at least $1,042.86 during the KiwiSaver financial year (1 July – 30 June).

The module: 1. Identifies inbound KiwiSaver contribution credits in the customer's transaction history (employer contribution direct credits, voluntary top-ups) 2. Sums year-to-date contributions 3. Computes the gap between YTD contributions and $1,042.86 4. Calculates weeks remaining in the KiwiSaver year 5. Publishes mtc_gap_nzd, mtc_shortfall_per_week, mtc_full_credit_achievable to wealth.kiwisaver_health

This indicator is only computed for NZ customers with a KiwiSaver account linked via MOD-100. It does not recommend contribution amounts — it reports facts about the customer's current trajectory.

PIR mismatch risk

Incorrect PIR (Prescribed Investor Rate) is a widespread and under-served problem in NZ. Members on the wrong rate pay too much or (technically) too little tax. The three rates are 10.5%, 17.5%, and 28%.

The module: 1. Sums gross salary credits observed in the customer's transaction history over the trailing 12 months (identifies likely income band) 2. Maps this to the applicable PIR band under the Income Tax Act 2007 s HL 21 3. Compares the inferred PIR to the PIR reported by the KiwiSaver provider via Akahu 4. Sets pir_mismatch_risk: true if the bands differ 5. Surfaces as an in-app insight: "Your KiwiSaver tax rate may not match your income — check with your provider."

This is an observation, not a directive. No tax advice is provided.

Snowflake schema

wealth.net_worth_daily (Dynamic Table, 1-hour lag)

Column Type Description
customer_id uuid Customer reference
as_at_date date Effective date of snapshot
instant_access_nzd numeric(18,2) Transaction + savings balances
short_term_locked_nzd numeric(18,2) Term deposits and notice accounts
illiquid_equity_nzd numeric(18,2) Property equity estimate
retirement_locked_nzd numeric(18,2) KiwiSaver + AU super balance (NZD)
total_assets_nzd numeric(18,2) Sum of all asset tiers
total_liabilities_nzd numeric(18,2) All outstanding loan balances
net_worth_nzd numeric(18,2) Assets minus liabilities
computed_at timestamptz When this row was last computed

wealth.kiwisaver_health (Dynamic Table, daily)

Column Type Description
customer_id uuid
ks_year_start date Current KiwiSaver year start (1 July)
ytd_contributions_nzd numeric(10,2) Observed YTD contributions
mtc_threshold_nzd numeric(10,2) $1,042.86
mtc_gap_nzd numeric(10,2) Gap to full government match (0 if met)
mtc_full_credit_achievable boolean True if gap closeable in remaining weeks
inferred_pir_pct numeric(4,1) PIR inferred from 12m salary history
reported_pir_pct numeric(4,1) PIR reported by provider via Akahu
pir_mismatch_risk boolean True if inferred ≠ reported

FMA compliance note

All outputs are factual computations on the customer's own data. No statements are made about which funds, providers, or products a customer should choose. The module does not constitute "personalised financial advice" under the Financial Markets Conduct Act 2013 s 431C. Display copy in the app is reviewed for advice boundary compliance before release.

Policies satisfied:

(No policies assigned)


MOD-105 — Product eligibility engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-105 is the centralised product eligibility gate. Given a party_id and product_id, it returns ELIGIBLE or INELIGIBLE with a structured reason code. It is called at two points: at offer generation time (MOD-108) to determine which products can be offered, and at application time to prevent ineligible customers applying via any channel (app, agent, API).

Why it exists

Compliance. CON-006 (Product suitability and governance) requires that products are only offered to eligible customers. No product offer may be generated without a passing eligibility check. CON-001 (Fair conduct) requires eligibility logic to be documented, auditable, and not discriminatory.

Commercial. Unsuitable product origination is a primary source of early credit losses. A centralised gate ensures that underwriting rules, CDD requirements, and exposure limits are enforced consistently at the point of offer rather than discovered later in the origination flow. MOD-105 also feeds MOD-107 (NBP engine) with the eligible product set for each customer, enabling personalised recommendations scoped to what the customer can actually receive.

Eligibility dimensions

KYC / CDD tier. Each product has a minimum CDD tier requirement (Simplified, Standard, or Enhanced). The customer's current tier is read from banking.customer_relationships.cdd_tier as written by MOD-010. A product with min_cdd_tier = ENHANCED is not offered to a customer at Standard CDD tier. Reason code: CDD_TIER_INSUFFICIENT.

Credit risk rating. Credit and lending products carry a minimum internal credit rating floor on the 1–10 scale produced by MOD-028. A customer whose current rating falls below the floor for a given product is ineligible. Reason code: CREDIT_RATING_BELOW_FLOOR.

Jurisdiction. Each product declares an eligible jurisdiction set (NZ, AU, or NZ+AU). The customer's custom:jurisdiction attribute must be a member of that set. Reason code: JURISDICTION_NOT_ELIGIBLE.

Existing product holdings. Cross-sell eligibility rules encode three constraint types: prerequisite products (some products require another product to already be held — for example, an overdraft facility requires an active everyday account); maximum holdings per customer (some products may only be held once); and mutual exclusion pairs (some products cannot be held simultaneously). Rules are stored in the product_eligibility.eligibility_rules configuration table and evaluated against current holdings at call time. Reason code: PRODUCT_HOLDINGS_CONSTRAINT.

Maximum total credit exposure. For credit products, the proposed new credit limit is added to the sum of all existing credit limits across the customer's portfolio. If this total exceeds the customer's maximum allowable exposure — derived from the affordability assessment in MOD-027 — the product is ineligible. Reason code: TOTAL_EXPOSURE_EXCEEDED.

Customer tenure. Some products have a minimum account tenure requirement before they can be offered (for example, 90 days of active banking relationship). This is evaluated against banking.customer_relationships.onboarded_at. Reason code: TENURE_INSUFFICIENT.

ROTE minimum. For credit and savings products, MOD-106 computes whether the product's projected ROTE exceeds the configured hurdle rate for that product class. Products persistently below hurdle are excluded from the eligible offer set until repriced. This gate is optional per product — products not yet covered by MOD-106 skip this check. Reason code: BELOW_ROTE_HURDLE.

Data model

-- product_eligibility.eligibility_results (Snowflake — batch evaluation, written nightly)
CREATE TABLE product_eligibility.eligibility_results (
  party_id          VARCHAR NOT NULL,
  product_id        VARCHAR NOT NULL,
  jurisdiction      VARCHAR NOT NULL,
  eligible          BOOLEAN NOT NULL,
  reason_code       VARCHAR,           -- NULL if eligible; e.g. CREDIT_RATING_BELOW_FLOOR
  reason_detail     VARCHAR,
  evaluated_at      TIMESTAMP_NTZ NOT NULL,
  model_version     VARCHAR NOT NULL,
  PRIMARY KEY (party_id, product_id, evaluated_at)
);

-- product_eligibility.eligibility_rules (Postgres — bank_risk, read by real-time API)
CREATE TABLE product_eligibility.eligibility_rules (
  product_id          text PRIMARY KEY,
  min_cdd_tier        text,
  min_credit_rating   int,
  jurisdictions       text[],
  min_tenure_days     int,
  max_per_customer    int,
  required_products   text[],
  excluded_products   text[],
  rote_hurdle_rate    numeric(5,4),
  effective_from      date NOT NULL,
  effective_to        date
);

Real-time vs batch evaluation

Nightly batch runs in Snowflake produce the full eligibility matrix across all active customers and products and write results to product_eligibility.eligibility_results. This output is the source for offer generation in MOD-108.

Real-time eligibility checks at application time use a lightweight Postgres read from the cached rules table combined with live state lookups for KYC tier and credit rating. This path must return within p99 ≤ 100 ms to remain non-blocking in the origination flow. The real-time check is considered authoritative at application time — stale batch results are not used for application gating.

Events

MOD-105 publishes product.eligibility_evaluated to the bank-risk-platform EventBridge bus after each nightly batch run completes. The event carries: party_id, eligible_product_count, ineligible_product_count, and evaluated_at. This event is consumed by MOD-107 to trigger refresh of the NBP candidate set for each customer.

Policies satisfied:

Policy Mode Description
CON-006 — Product suitability and governance GATE Every product offer or application is gated against the eligibility matrix — a customer not eligible for a product cannot be presented with it or apply for it.
CON-001 — Customer Fairness & Conduct Policy GATE Eligibility evaluation considers existing exposure, customer segment, and product complexity tier — ensuring products are not offered to customers for whom they are unsuitable.
CRE-001 — Credit Risk Management Policy GATE Credit product eligibility enforces maximum DTI, credit tier floor, and CDD tier requirements before the product can be offered or applied for.

MOD-106 — ROTE engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-106 computes daily ROTE (Return on Tangible Equity) at three levels: per product per customer (facility-level), per product across the portfolio (product-level), and per customer across all their holdings (customer-level). Results feed MOD-105 (eligibility — ROTE hurdle gate) and executive dashboards via Snowflake.

Why it exists

Governance. CON-006 requires that products persistently below the ROTE hurdle rate are flagged to the product governance board. Without a systematic calculation this obligation is manual, inconsistent, and difficult to evidence to the regulator.

Commercial. Return on tangible equity is the primary capital efficiency metric used by the board to evaluate product portfolio performance. Facility-level ROTE shows whether individual credit exposures are generating adequate return relative to the regulatory capital they consume. Customer-level ROTE tells the bank whether a given customer relationship is profitable in aggregate — informing product selection in MOD-107 (NBP engine) and pricing adjustments.

ROTE formula

Facility ROTE =  (NIM Revenue − ECL Provision − Operating Cost Allocation)
                 ─────────────────────────────────────────────────────────
                     Allocated Regulatory Capital (RWA × CET1 floor)

Where:
  NIM Revenue               = from MOD-086 FTP grid (product interest income minus transfer price cost)
  ECL Provision             = from MOD-031 (expected credit loss for this exposure)
  Operating Cost Allocation = configurable cost coefficient per product class (bps of balance)
  Allocated Regulatory Capital = product exposure × RWA risk weight (from MOD-033) × CET1 floor (config)

Tangible equity is approximated as RWA × CET1 minimum ratio, configured per jurisdiction: NZ uses 6% per RBNZ requirements; AU uses 4.5% plus applicable buffers per APRA requirements. This gives a consistent capital allocation across the portfolio without requiring a full internal capital adequacy model.

Data model

-- rote.facility_rote (Snowflake — written daily by MOD-106)
CREATE TABLE rote.facility_rote (
  party_id                     VARCHAR NOT NULL,
  product_id                   VARCHAR NOT NULL,
  account_id                   VARCHAR NOT NULL,
  jurisdiction                 VARCHAR NOT NULL,
  nim_revenue                  NUMBER(18,6) NOT NULL,
  ecl_provision                NUMBER(18,6) NOT NULL,
  operating_cost_allocation    NUMBER(18,6) NOT NULL,
  allocated_regulatory_capital NUMBER(18,6) NOT NULL,
  rote_annualised              NUMBER(8,6) NOT NULL,   -- e.g. 0.1234 = 12.34%
  hurdle_rate                  NUMBER(8,6) NOT NULL,
  below_hurdle                 BOOLEAN NOT NULL,
  calculation_date             DATE NOT NULL,
  model_version                VARCHAR NOT NULL
);

-- rote.customer_rote (Snowflake — daily roll-up across all customer facilities)
CREATE TABLE rote.customer_rote (
  party_id                  VARCHAR NOT NULL,
  jurisdiction              VARCHAR NOT NULL,
  total_nim_revenue         NUMBER(18,6) NOT NULL,
  total_ecl_provision       NUMBER(18,6) NOT NULL,
  total_operating_cost      NUMBER(18,6) NOT NULL,
  total_allocated_capital   NUMBER(18,6) NOT NULL,
  customer_rote_annualised  NUMBER(8,6) NOT NULL,
  calculation_date          DATE NOT NULL,
  PRIMARY KEY (party_id, calculation_date)
);

Hurdle rate framework

Each product class has a configured ROTE hurdle rate stored in product_eligibility.eligibility_rules.rote_hurdle_rate and read by both MOD-105 and MOD-106 at calculation time. Representative hurdle rates: personal loan 15%, everyday account 8%, term deposit 10%. Rates are set by the product governance board and updated through a controlled configuration change process.

If a facility's annualised ROTE falls below the configured hurdle for 90 consecutive calendar days, the record is written with below_hurdle = true. MOD-105 reads this flag at eligibility evaluation time — products in a persistent below-hurdle state have their ROTE gate fail for new offers until the product is repriced or the hurdle rate is formally revised. An alert is sent to the product governance board via MOD-076 when a product first enters or exits a below-hurdle state.

Customer-level roll-up

Customer ROTE is computed as the sum of all facility-level net returns (NIM revenue minus ECL provision minus operating cost allocation) divided by total allocated regulatory capital across all the customer's holdings. This produces a single capital-efficiency figure for the entire customer relationship.

MOD-107 (NBP engine) consumes customer ROTE as an input to next best product scoring. Customers with low overall ROTE are prioritised for upsell opportunities in higher-returning product categories, subject to eligibility gating by MOD-105.

Events

MOD-106 publishes rote.facility_rote_calculated to the bank-risk-platform EventBridge bus after each daily batch run completes. The event carries summary statistics: below-hurdle count by product class, average annualised ROTE by product class, and calculation date. This event is consumed by MOD-105 to refresh the ROTE gate state for the following day's eligibility evaluations.

Policies satisfied:

Policy Mode Description
CON-006 — Product suitability and governance CALC Computes product-level and customer-level ROTE as a governance input to product eligibility and pricing decisions — products persistently below hurdle are flagged for product governance board review.
CRE-001 — Credit Risk Management Policy CALC Risk-adjusted return calculated using RWA-based regulatory capital, ECL provision, and NIM attribution — providing a capital-efficiency view of credit product performance.

MOD-107 — Next best product engine

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

What it does

MOD-107 computes a ranked list of next best products for every active customer on a weekly Snowflake run. It takes the eligible product set from MOD-105, scores each eligible product against a set of customer signals, and writes the top 3 ranked products per customer to Postgres for consumption by MOD-108 (offer engine), MOD-083 (agent assist back-office view), and MOD-077 (app insight cards).

Why it exists

Commercial reason: Next best product is the primary mechanism for deepening customer relationships. The bank's growth model (BG-003, BG-005) depends on expanding product holdings per customer — an NBP recommendation that surfaces the right product at the right moment drives conversion without requiring a sales team.

Conduct reason: Scoping recommendations to the eligibility matrix (MOD-105) ensures every surfaced product is one the customer can actually obtain. This directly satisfies CON-006 (product suitability) and avoids the mis-selling risk of promoting products a customer would be declined for.

Ranking model

Signal Source Weight direction
Eligible product (binary gate) MOD-105 Must be ELIGIBLE — ineligible products excluded
Relationship gap Products held vs products in same segment peer group Higher gap → higher rank
Churn / health score MOD-040 High churn risk → prioritise retention products (savings rate, loyalty offer)
Customer ROTE contribution MOD-106 Products that improve customer-level ROTE ranked higher
Behavioural signals Spend categories, idle cash detected, FX activity Idle cash → savings product; regular FX → multi-currency wallet
Recency of prior offer MOD-108 offer history Products offered and rejected recently ranked lower (cooling-off period: 90 days)
Product lifecycle stage Product register Products in sunset / under-capacity ranked lower

Weights are configurable per product class in a Snowflake configuration table. Model version is recorded on every output row.

Data model

-- product_intelligence.next_best_product (Snowflake — written weekly)
CREATE TABLE product_intelligence.next_best_product (
  party_id            VARCHAR NOT NULL,
  jurisdiction        VARCHAR NOT NULL,
  rank                INT NOT NULL,           -- 1 = highest ranked
  product_id          VARCHAR NOT NULL,
  score               NUMBER(8,6) NOT NULL,   -- 0.0–1.0
  primary_signal      VARCHAR NOT NULL,       -- top-contributing signal name
  model_version       VARCHAR NOT NULL,
  evaluated_at        TIMESTAMP_NTZ NOT NULL,
  PRIMARY KEY (party_id, rank, evaluated_at)
);

Results are written back to Postgres (product_intelligence.nbp_current) for fast app reads — only the current week's top 3 per customer are held in Postgres; Snowflake retains full history.

Output destinations

  • MOD-108 reads NBP rankings to prioritise which eligible products generate offers.
  • MOD-083 (agent assist) surfaces the top 3 NBP in the agent's customer 360 view.
  • MOD-077 (app dashboard) reads NBP for insight cards — surfaces as contextual prompts.

Fairness monitoring

Weekly fairness report computes recommendation rate by jurisdiction, age band, and customer segment. Any product where recommendation rate differs by more than 10 percentage points across demographic groups triggers a nbp.fairness_breach alert to the compliance team for model review.

Policies satisfied:

Policy Mode Description
CON-006 — Product suitability and governance CALC NBP recommendations are scoped strictly to the customer's eligible product set from MOD-105 — no product outside the eligibility matrix can appear as a next best product.
CON-001 — Customer Fairness & Conduct Policy CALC Ranking model is periodically tested for demographic fairness — no protected characteristic may systematically suppress product recommendations for a customer group.

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

Continuously monitor credit exposures to related parties and alert the board and risk function when exposures approach or breach the quantitative limits set by the NZ DTA Related Party Exposures Standard and equivalent APRA requirements. The module eliminates the manual aggregation burden that makes related party monitoring error-prone, and provides a single authoritative view of related party exposure as a percentage of Tier 1 capital.

What it does

The module maintains a registry of persons and entities designated as related parties of the deposit taker. The compliance team manages designations through a governed interface. The registry covers directors, senior managers, their associates, entities in which they hold a material interest, and parent/subsidiary entities. Designations are linked to customer CDD profiles (MOD-010) via the risk.related_party_designations table, which holds: designation_id, customer_id, relationship_type (director / senior_manager / associate / parent / subsidiary / other), designated_by, designated_at, and valid_until. Expired designations are retained for audit history.

Exposure calculation

The module aggregates all on-balance-sheet credit exposures — loans, overdrafts, and guarantees — per related party counterparty, sourcing outstanding balances from the MOD-001 core ledger. Off-balance-sheet committed facilities are included at their full undrawn value, consistent with prudential exposure measurement requirements. Each aggregated counterparty total is expressed as a percentage of the current Tier 1 capital figure supplied by MOD-033. Calculations run continuously as ledger events arrive; a full reconciliation sweep also executes nightly.

Exposure data is persisted in the risk.related_party_exposures table: exposure_id, related_party_id, exposure_type (loan / overdraft / guarantee / facility), outstanding_balance, committed_undrawn, total_exposure, tier1_capital_at_calc, exposure_pct, limit_pct, headroom_pct, breach_flag, calculated_at.

Jurisdiction limits

The NZ DTA Related Party Exposures Standard sets per-counterparty and aggregate limits as a percentage of Tier 1 capital. Exact threshold values are configurable parameters in the platform and will be populated when the finalised standard is published by the RBNZ. For AU-licensed operations, APS 222 (Connected Lending) equivalent limits apply and are held in a parallel configuration set. The module applies the correct limit parameters based on the jurisdiction of each entity in the exposure register.

Alert thresholds and notifications

Warning and breach thresholds are configurable. The default configuration raises a warning alert at 80% of the applicable limit and a breach alert at 100%. Warning alerts are routed to the Chief Risk Officer and CFO. Breach alerts are escalated to those recipients plus the Board Risk Committee chair. All alerts are recorded as immutable entries via MOD-048 before dispatch.

Dashboard

A real-time related party exposure dashboard is available to users holding the risk_officer or board_risk platform role. The dashboard shows all designated related parties, their current aggregate exposure amount, applicable limit, headroom in dollar and percentage terms, and a 90-day trend sparkline. Filters by relationship type and jurisdiction are available. Breach rows are highlighted; any row above the warning threshold is flagged amber.

Quarterly board report extract

The module generates a formatted PDF extract of the full related party exposure register suitable for inclusion in the board risk report. The extract is produced on a scheduled basis aligned to the board reporting calendar and shows the position at the reporting date, comparative figures for the prior quarter, and any breaches or near-misses that occurred during the period.

Compliance reason

The RBNZ DTA Related Party Exposures Standard is a prudential standard under the Deposit Takers Act 2023. Breaching per-counterparty or aggregate related party exposure limits constitutes a licence condition breach and may trigger mandatory supervisory reporting obligations. Without automated monitoring, aggregating exposures across all loan accounts, facilities, and guarantees for every designated related party requires manual reconciliation that is prone to omission — particularly where a related party holds multiple products or where a new designation is added mid-cycle. The module makes breach impossible to miss and retains the full calculation history available for RBNZ examination.

Commercial reason

Related party lending failures have been a leading cause of bank collapses in New Zealand and internationally. Automated monitoring protects the institution from inadvertent breaches by ensuring that no new credit to a related party is advanced without a clear view of the post-advance exposure percentage. The real-time headroom figure gives the credit team a definitive, auditable answer to whether additional credit to a related party counterparty can be extended at any given moment, removing reliance on ad hoc spreadsheet checks.

Policies satisfied:

Policy Mode Description
GOV-009 — Related Party Transactions Policy CALC Related party credit exposures are calculated continuously as a percentage of Tier 1 capital and compared against regulatory limits — no manual aggregation required.
GOV-009 — Related Party Transactions Policy ALERT Alerts are generated when any single related party exposure approaches or breaches the DTA limit, giving the board time to act before a breach occurs.
CRE-005 — Concentration Risk Policy ALERT Related party concentrations are included in the concentration risk monitoring dashboard alongside other large-exposure alerts.

MOD-150 — Risk management platform

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

The Risk Management Platform is the automated risk intelligence and registration layer for the platform. It ingests events from every system domain, classifies them against the risk taxonomy (D01–D11), maintains the operational risk register without manual entry, monitors all critical third-party relationships, runs the Risk Appetite Framework (RAF) dashboard, maintains the model inventory, and triggers regulatory breach notifications automatically.

Every risk event that requires human judgement is elevated to the Risk Case Console (MOD-151) as a case. Everything else is logged and closed without human involvement. The operating principle is that a risk manager's time is for assessing exceptions, not recording events.

Operational risk register

All events ingested from MOD-076 (observability alerts), MOD-048 (system decision log), MOD-047 (agent action log), AWS CloudTrail (IAM and API events), and CI/CD pipeline webhooks are classified against the risk taxonomy and written as entries to risk.operational_risk_events. A rules engine performs classification: error type, source module, affected domain, and severity determine the risk category. Entries record: event_id, source_module, risk_domain (D01–D11), event_type, severity (P1/P2/P3), description, occurred_at, auto_resolved (bool), resolution_timestamp, and related_incident_id.

The register is append-only. No event is ever deleted or modified.

RCSA (Risk and Control Self-Assessment) metrics are derived automatically from control test pass/fail rates in CI pipelines. A control that repeatedly fails testing auto-generates a risk register entry under OPS-004, creating a direct link between engineering quality signals and the formal risk register — without any manual intervention.

Risk Appetite Framework dashboard

Aggregates all quantitative RAF indicators from SD06 into a single continuously updated view. Indicators include:

  • CET1 ratio (MOD-033)
  • LCR and NSFR (MOD-032)
  • IRRBB EVE sensitivity (MOD-035)
  • Stress test capital adequacy (MOD-034)
  • Related party exposure % (MOD-147)
  • High-risk customer concentration (MOD-039)
  • Operational loss trend (from the risk event register)

Each indicator has a configured RAF threshold. A breach triggers an automatic alert to the CRO, CFO, and Board Risk Committee chair. The board risk report is auto-generated on the board reporting calendar cadence, pulling live values and 90-day trends for all indicators. No spreadsheet is involved in its production.

Model inventory and lifecycle management

Every model deployment event from the CI/CD pipeline — model ID, version, training data lineage hash, feature set version, and validation report reference — is written to risk.model_inventory. The inventory records: model_id, owner_module, model_type (ML / statistical / rules), deployment_status (development / awaiting_validation / production / retired), deployed_at, last_validated_at, next_review_at, performance_thresholds (JSONB), current_psi, current_accuracy, current_recall, champion_id (nullable), challenger_id (nullable).

Nightly jobs run PSI and accuracy computations for all production models against the latest population. A threshold breach auto-creates an incident and flags the model for priority review. A model that has not been validated within its review SLA is flagged and its owner notified.

The validation gate is hard: no model can be promoted to production status in the inventory without a validation report reference attached. This constraint is checked by the CI/CD hook before deployment proceeds — model promotion is not a UI action, it is an outcome of a completed validation case in MOD-151.

Third-party health monitoring

All designated critical third-party services are registered in risk.critical_service_providers with: provider_name, service_type, dependency_modules (list), sla_uptime_pct, sla_latency_ms, health_check_endpoint, contractual_review_date, and tier (critical / important / standard).

Health checks run every 60 seconds. Neon database connection latency, Snowflake query latency, AWS service health, BPAY API availability, NPP connectivity, and eIDV provider response times are all monitored. When a check fails or an SLA metric is breached, an incident is auto-created with the provider name, metric, breach value, and a dependency graph showing which platform modules are affected. Contractual review dates approaching within 90 days auto-generate a reminder case in MOD-151.

Intraday liquidity monitoring

Payment events from MOD-020 are aggregated in real-time to produce a running intraday liquidity position. The position tracks: gross inflows (incoming payments received), gross outflows (outgoing payments settled), net intraday position, peak intraday exposure to each payment system (NPP, BPAY, NZ Faster Payments), and available intraday credit headroom. Positions are stored in Snowflake Dynamic Tables with a 5-minute refresh.

When intraday exposure exceeds the configured limit, an alert is sent to Treasury and the CRO. End-of-day positions feed into the MOD-032 LCR calculation as the final liquidity position for the day, completing the loop between intraday tracking and end-of-day regulatory reporting.

Incident and breach auto-creation

When MOD-076 fires a P1 or P2 alert, this module auto-creates an incident record in risk.incidents: incident_id, severity (P1/P2/P3), alert_source, alert_code, description, created_at, sla_resolve_by, status, regulatory_notification_required (bool), regulatory_notification_sent_at (nullable).

P1 incidents with regulatory_notification_required = true auto-assemble a notification document citing the incident, the affected service, the estimated customer impact, and the current resolution status. Where a regulator API exists — RBNZ incident notification portal, APRA breach notification API — the notification is submitted automatically within the required window. Where no API exists, a draft is staged in MOD-151 for human review before submission.

Change management feed

Every CI/CD deployment event — success, failure, rollback — creates a change record in risk.change_records: change_id, environment, module_id, artefact_hash, deployed_by (CI pipeline identity), deployed_at, outcome, rollback_of (nullable), post_impl_review_required (bool), post_impl_review_due_at (nullable). Post-implementation review is auto-required for any deployment that resulted in a rollback, or where a P1 incident occurred within 72 hours of deployment. Review cases are created in MOD-151.

Compliance rationale

RBNZ Operational Resilience Standard and APRA CPS 230 require that operational risk events are identified, classified, and managed within defined timeframes. OPS-003 through OPS-007 encode these obligations. Automation is not a convenience here — regulators assess the timeliness and completeness of risk event capture. A manual register populated by a human reviewing alert emails is inherently incomplete: it misses events that occur outside business hours, events that occur simultaneously, and events that are never reviewed because the reviewer is handling a higher-priority incident. This module makes completeness structurally guaranteed.

APRA CPS 220 and RBNZ technology risk guidance (DT-003, DT-008) require ongoing monitoring of technology risks and third-party providers. Continuous automated health checking against configured SLA thresholds is the only approach that satisfies the "ongoing" requirement at the pace of a digital bank's operational tempo.

Model risk under APRA CPS 220 (DT-005) requires a documented model inventory and validation process. Deriving the inventory automatically from CI/CD events means it is always current — a manually maintained spreadsheet will always lag deployments.

GOV-002 (RAF requirements) under both RBNZ and APRA frameworks requires the board to have visibility of risk appetite indicators on a regular basis. Auto-generating the board report from live data eliminates the lag and manual error inherent in compiled spreadsheet packs.

REP-009 encodes mandatory breach notification timelines: RBNZ requires notification within 24 hours of a material incident; APRA requires notification within 24 hours under CPS 234. PRI-002 encodes the NZ Privacy Act 2020 mandatory breach reporting obligation and the AU Notifiable Data Breaches (NDB) scheme. The assembly and submission workflow means the clock stops ticking on notification compliance when the automated submission lands, not when a compliance officer finishes drafting an email.

Commercial rationale

The cost of a late or incomplete breach notification — regulatory censure, public disclosure, reputational damage — far exceeds the cost of automating the process. A risk manager spending their time manually entering events into a register is not doing risk management; they are doing data entry. This module eliminates data entry entirely. The risk function's attention is reserved for the cases that require it.

A continuously computed RAF dashboard also eliminates the quarterly board pack compilation cycle — a process that typically consumes two to three person-weeks of finance and risk staff time per quarter, and produces a snapshot that is already weeks old by the time it reaches the board.

Policies satisfied:

Policy Mode Description
OPS-003 — Incident Management Policy AUTO Incidents are auto-created from observability alerts with P1/P2/P3 classification, SLA timers, and routing — no manual incident registration required.
OPS-004 — Operational Risk Policy AUTO Risk events from all system domains are auto-classified against the risk taxonomy and written to the operational risk register continuously — no manual entry.
OPS-005 — Third-Party & Critical Service Provider Policy AUTO All designated critical third parties (Neon, Snowflake, AWS, BPAY, NPP, eIDV providers, card bureau) are continuously health-monitored; SLA breach auto-creates an incident.
OPS-006 — Change Management Policy LOG CI/CD pipeline deployment events auto-create change records with timestamp, artefact hash, environment, and outcome; post-implementation review is auto-scheduled for P1 changes.
OPS-007 — Financial Processing Resilience & Idempotency Policy LOG Idempotency key collision rates, reprocessing events, and settlement reconciliation outcomes are continuously tracked and logged against this policy.
DT-003 — Technology Risk Management Policy AUTO Technology risk events (unpatched CVEs from SAST, latency SLA breaches, infrastructure anomalies) are auto-classified and written to the risk register.
DT-005 — Model Risk Management Policy LOG Model inventory is auto-maintained from CI/CD deployment events; scheduled PSI and accuracy monitoring runs nightly; model validation gate is enforced before production promotion.
DT-008 — Third-Party & Outsourcing Risk Policy AUTO All designated critical third-party services are continuously monitored for health and SLA compliance; contract expiry dates trigger review reminders.
GOV-002 — Risk Appetite Statement Policy CALC The RAF dashboard is continuously computed from SD06 outputs; RAF threshold breach auto-alerts the CRO and Board Risk Committee chair.
REP-009 — Regulatory incident & breach notification AUTO Material incidents and privacy breaches are auto-detected and routed through a notification assembly workflow; regulator API submission proceeds where an API is available.
PRI-002 — Data Breach Response Policy AUTO Security anomalies (CloudTrail access failures, Cognito brute-force patterns, Secrets Manager anomalies) are auto-classified as potential breaches and the notification timer starts automatically.
CLQ-002 — Liquidity Risk Management Policy CALC Intraday payment system exposure is computed in real-time from the payment event stream, extending MOD-032's end-of-day LCR calculation to cover intraday exposure.

MOD-152 — Climate risk assessment

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

The climate risk assessment module provides continuous automated assessment of climate-related financial risk across the platform's lending portfolio. It integrates physical risk (property-level hazard exposure for mortgage collateral) and transition risk (high-carbon sector concentration in the lending book), feeds climate indicators into the Risk Appetite Framework dashboard, registers climate stress scenarios with the stress testing engine, and generates the annual TCFD-aligned climate disclosure report from live model outputs. No manual climate risk assessment is required.

What it does

Physical risk assessment for mortgage collateral

Integrates with a configurable third-party property climate hazard API (RiskSmart, NIWA NZ, or equivalent AU provider) to obtain physical risk scores for all properties held as mortgage collateral. Scores cover: flood risk (1-in-100-year and 1-in-200-year event probability), wildfire risk, coastal inundation risk under sea level rise scenarios (+0.5m, +1.0m, +2.0m), and an aggregate physical risk tier (low / medium / high / very high). Scores are refreshed annually and whenever the hazard API publishes a material dataset update. Each property's risk tier is stored in risk.property_climate_risk with the LVR and outstanding balance from the core ledger. Portfolio-level metrics: total mortgage book exposure by risk tier, geographic concentration (territorial authority / postcode), and LVR-weighted average exposure per tier. When physical risk concentration in any tier exceeds the configured RAF threshold, the CRO is alerted automatically.

Transition risk — sector concentration analysis

Each business and SME lending counterparty is classified by ANZSIC sector code drawn from their CDD profile (MOD-010). ANZSIC codes are mapped to a climate transition risk tier using TCFD sector guidance and RBNZ/APRA classifications: high-transition-risk sectors include fossil fuel extraction, heavy manufacturing, high-emissions-intensity agriculture, aviation, and cement/steel production. Portfolio concentration by transition risk tier is computed continuously and monitored against configured RAF limits. Over-concentration in high-transition-risk sectors triggers an advisory alert to the CRO and credit risk team.

Climate stress scenarios

Two climate stress scenario types are registered with the stress testing engine (MOD-034): (1) acute physical risk — a severe flood event affecting the top decile of highest-risk postcodes in the mortgage book, modelling collateral value impairment and resulting credit losses; (2) rapid policy transition — a carbon price shock causing credit deterioration in high-transition-risk lending sectors, modelled as a probability-of-default uplift and LGD adjustment. Both scenarios follow RBNZ FSAP specification templates. Results are expressed as CET1 basis point reduction and are included in the ICAAP stress section from MOD-034.

TCFD disclosure report

On the configured annual schedule, the module generates a TCFD-aligned climate disclosure covering the four TCFD pillars. The Governance and Strategy narrative sections are template-based and require institution input before publication; the Risk Management and Metrics & Targets sections are populated automatically from live climate risk model outputs: portfolio physical risk exposure by tier, transition risk sector concentration, climate stress test results, and year-on-year trend for each indicator. The completed document is stored as an immutable record in bank-reports-prod S3 with a version hash.

Compliance reason

The NZ Climate-related Disclosures Act 2021 mandates TCFD-aligned disclosure for in-scope NZ financial institutions from FY2023. APRA CPG 229 requires AU ADIs to identify, assess, and manage climate risk within their existing risk frameworks. The RBNZ has signalled climate stress testing as part of its supervisory programme. Mortgage portfolios carry material physical risk from flood, wildfire, and coastal inundation — risks that are not reflected in historical credit models and require a dedicated assessment capability.

Commercial reason

Understanding which postcodes in the mortgage book carry the highest physical climate risk, and what proportion of the book those postcodes represent, is fundamental portfolio risk management. The module converts property data already held in the platform and a third-party hazard API feed into a continuously maintained climate risk view that would otherwise require a significant manual exercise — and which most deploying institutions do not have the in-house capability to build.

Policies satisfied:

Policy Mode Description
CLQ-007 — Climate Risk Management Policy CALC Physical and transition risk scores computed continuously across the lending portfolio and integrated with the stress testing engine — no manual climate risk assessment required.
REP-012 — TCFD Climate Disclosure Policy AUTO TCFD-aligned climate disclosure report generated from live model outputs on annual schedule — metrics and targets section fully automated.
GOV-002 — Risk Appetite Statement Policy ALERT Climate risk indicators (physical risk concentration, high-transition-risk sector exposure) included in the RAF dashboard; breach of climate risk appetite limits triggers automatic Board alert.

MOD-165 — Synthetic swap book aggregator

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Purpose

Maintains the bank's synthetic swap book — the internal treasury representation of the interest-rate exposure arising from all active fixed-rate components across all Flexible Loan Facilities (PRD-024). Aggregates component-level positions into standard maturity repricing buckets, assigns matched-tenor FTP rates, and computes daily EVE and NII sensitivity metrics under regulatory rate-shift scenarios. This is the module that connects the product-level fixed-rate component data (MOD-162) to the IRRBB capital and regulatory reporting frameworks.

Context

Every fixed-rate loan component is, from a treasury hedging perspective, a pay-fixed / receive-floating interest rate swap. When aggregated across the entire FLF book, these positions form a synthetic swap book: a ladder of known fixed cash flows by maturity bucket. RBNZ BS13 and APRA APS-117 both require banks to measure and capitalise this exposure. Without this module, the IRRBB gap would need to be computed manually from loan data, which is both error-prone and incompatible with the REP-004 AUTO posture.

The FTP dimension is equally important. Each fixed component is funded internally by treasury at the matched-tenor wholesale rate (from MOD-161). This isolates the lending business's net interest margin from rate movements — the lending business earns (contracted_rate - FTP_rate) regardless of what happens to market rates; treasury manages (FTP_rate - market_rate) risk across the aggregated book. Without FTP assignment at component level, P&L attribution between the lending business and treasury is impossible.

Data model

risk.synthetic_swap_positions

One row per active fixed-rate component position. Updated on component lifecycle events from MOD-162.

Column Type Notes
id uuid PK
component_id text NOT NULL — cross-system reference to credit.loan_facility_components.id
facility_id text NOT NULL — cross-system reference to credit.loan_facilities.id
customer_id text NOT NULL
jurisdiction char(2) NOT NULL CHECK ('NZ','AU')
notional_amount numeric(18,2) NOT NULL
currency char(3) NOT NULL
contracted_rate numeric(8,6) NOT NULL — the fixed rate locked at component establishment
ftp_rate numeric(8,6) NOT NULL — matched-tenor FTP rate from MOD-161 at establishment
ftp_tenor_months int NOT NULL — the tenor used for FTP lookup
market_rate_at_establishment numeric(8,6) NOT NULL — mid-market rate from MOD-085 at establishment (for break-cost basis reference)
start_date date NOT NULL
maturity_date date NOT NULL
maturity_bucket text NOT NULL CHECK ('ON','1M','3M','6M','1Y','2Y','3Y','5Y','7Y','10Y','15Y','15Y_PLUS')
status text NOT NULL CHECK ('ACTIVE','MATURED','PREPAID')
source_event_id text NOT NULL — EventBridge event_id of the originating component_created event
trace_id text NULL
created_at timestamptz NOT NULL DEFAULT now()
last_updated timestamptz NOT NULL DEFAULT now()

Index: (status, maturity_bucket, jurisdiction) for bucket aggregation queries. (component_id) UNIQUE for idempotent event processing.

maturity_bucket is assigned at position creation using the standard Basel repricing buckets: positions with maturity in ≤1 month go to 1M, ≤3 months to 3M, etc. The ON bucket is used for floating-rate positions (not applicable for fixed components, reserved for future use).

risk.irrbb_repricing_summary

Daily snapshot of the aggregated synthetic swap book. One row per snapshot_date × jurisdiction × maturity_bucket × scenario.

Column Type Notes
id uuid PK
snapshot_date date NOT NULL
jurisdiction char(2) NOT NULL CHECK ('NZ','AU','COMBINED')
maturity_bucket text NOT NULL
scenario text NOT NULL CHECK ('BASE','UP_100','UP_200','UP_300','DOWN_100','DOWN_200','DOWN_300')
total_notional numeric(18,2) NOT NULL
position_count int NOT NULL
weighted_avg_contracted_rate numeric(8,6) NOT NULL
weighted_avg_ftp_rate numeric(8,6) NOT NULL
nim_contribution_annual numeric(18,2) NOT NULL — (contracted_rate - ftp_rate) × total_notional
eve_impact numeric(18,2) NULL — populated for non-BASE scenarios; PV change under rate shift
nii_impact_12m numeric(18,2) NULL — populated for non-BASE scenarios; 12-month NII change
market_rate_used numeric(8,6) NULL — the shocked rate applied (BASE = mid-market; shifted = BASE ± bps)
created_at timestamptz NOT NULL DEFAULT now()

UNIQUE on (snapshot_date, jurisdiction, maturity_bucket, scenario).

Mutable for correction runs (if a position is subsequently found to have been missing from a snapshot, the snapshot can be recomputed). Snapshots older than 90 days are immutable; a CHECK constraint on the handler prevents writes to old snapshots outside of a designated correction window.

Handlers

consume-component-event — SQS consumer of bank.credit.component_created and bank.credit.component_status_changed from the bank-credit bus (cross-bus rule; see MOD-104 note below). On component_created for a FIXED component: calls MOD-161 for the matched-tenor FTP rate; determines the maturity bucket; inserts a row into risk.synthetic_swap_positions. On component_status_changed to MATURED or PREPAID: updates the position status. Idempotent on component_id UNIQUE constraint.

daily-irrbb-sweep — EB Scheduler cron, runs at 08:00 NZST daily (after MOD-162's 06:00 maturity sweep ensures terminal positions are closed). Reads all ACTIVE positions. For each of seven scenarios (BASE + six parallel rate shifts), computes bucket-level aggregates and EVE/NII sensitivities. Inserts snapshot rows into risk.irrbb_repricing_summary. Publishes bank.risk.irrbb_snapshot_updated on the bank-risk-platform bus.

EVE and NII sensitivity calculation

EVE impact for a parallel rate shift of Δr basis points applied to scenario S:

eve_impact_S = Σ_positions [ notional × (annuity_factor(r_base, remaining_months) − annuity_factor(r_base + Δr, remaining_months)) ]

Where annuity_factor(r, n) is the present value factor for a fixed annuity at rate r over n months. This is the standard interest rate risk duration-based approximation. A positive EVE impact means the bank's economic value increases under the scenario (rates rose, fixed-rate positions are now more valuable to the bank as receiver).

NII impact over a 12-month horizon for scenario S:

nii_impact_S = Σ_positions_repricing_within_12m [ notional × (r_base − (r_base + Δr)) × (remaining_months / 12) ]

For fixed-rate components repricing within 12 months (i.e. maturing within the horizon), the NII impact is the loss of contracted margin relative to the new market rate on rollover. For components with maturities beyond the horizon, the NII impact is zero (the fixed rate is locked through the period).

Both metrics are computed per jurisdiction (NZ, AU) and combined, consistent with RBNZ BS13 and APRA APS-117 reporting requirements.

FTP assignment

At position creation, the module calls MOD-161 with the component tenor in months and jurisdiction. MOD-161 returns the current matched-tenor FTP rate. This rate is locked on the position record — it does not change for the life of the component even as market rates move. The FTP rate represents the bank's internal cost of term funding for this component; locking it at establishment ensures the lending business's NIM is known and stable from day one.

The NIM contribution (contracted_rate - ftp_rate) per component is the lending business's reward for originating the loan. Treasury earns or loses (ftp_rate - current_market_rate) on the hedge. These two P&L streams are separate and managed independently.

Cross-bus consumption

MOD-165 consumes from the bank-credit EventBridge bus while residing in bank-risk-platform. Before deployment, file MOD-104-bank-credit-consumption-grant-mod165.handoff.md to bank-platform requesting: - BankRiskPlatformRole: events:PutRule + events:PutTargets on the bank-credit EventBridge bus ARN

This is the same pattern as MOD-024's cross-bus consumption of fraud alert events. The SQS queue and EventBridge rule are provisioned by MOD-165's SST config; they require the IAM grant to resolve.

Events published

Event Bus Trigger
bank.risk.irrbb_snapshot_updated bank-risk-platform Daily sweep completes

Consumers: MOD-033 (capital ratio engine — IRRBB RWA input), MOD-042 (CDC ingestion for Snowflake analytics), regulatory reporting modules.

Implementation notes

The maturity bucket assignment uses the component's maturity_date relative to today's date at the time the position is created. As time passes and remaining terms shorten, positions do not automatically migrate between buckets in the position table — the daily sweep recomputes bucket assignments dynamically from current maturity dates when building the repricing summary. The position table's maturity_bucket column reflects the bucket at origination only; the summary is always computed fresh.

For v1, only fixed-rate components from PRD-024 are tracked. The position table is designed to accommodate other fixed-rate products (future: fixed-rate term deposits, fixed-rate mortgages managed outside MOD-116) via a product_id column which should be added in v2 when additional products are brought into the synthetic book.

The floating-rate component of each FLF facility is excluded from the synthetic swap book by design. Floating-rate exposure is naturally hedged (the bank funds floating and receives floating); it creates no IRRBB gap and requires no internal FTP swap. The gap report should confirm zero contribution from floating components.

Policies satisfied:

Policy Mode Description
CLQ-001 — Capital Adequacy Policy CALC IRRBB repricing gap and EVE/NII sensitivity metrics produced by this module feed the capital calculation engine (MOD-033) for the interest-rate risk in the banking book capital requirement; the daily snapshot is the authoritative input for IRRBB RWA contribution.
REP-002 — Prudential Reporting Policy CALC Component-level fixed cash flows and maturity-bucketed repricing summaries are computed and stored automatically after each daily sweep, forming the data source for RBNZ BS13 and APRA APS-117 regulatory IRRBB returns.
REP-004 — Financial Statements Policy AUTO Daily IRRBB snapshots — repricing buckets, EVE sensitivities, and NII sensitivities — are written to the reporting store without manual intervention; no manual journal or analyst-prepared spreadsheet is required for the IRRBB position.

MOD-170 — Regulatory Submissions Portal

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

The Regulatory Submissions Portal is the human interface for all regulatory return activity across SD06. It aggregates submission calendars, assembled return data, approval workflows, and historical submission records from MOD-036 (prudential returns), MOD-037 (AUSTRAC/RBNZ AML reports), MOD-057 (statistical surveys), and MOD-060 (FATCA/CRS/AEOI) into a single Snowflake Streamlit application.

Why it exists

The downstream modules (MOD-036 etc.) are data pipelines — they assemble, validate, and submit regulatory returns automatically. But automated submission without human sign-off is unacceptable for returns filed with RBNZ and APRA. A defective prudential return filed incorrectly triggers regulatory sanctions and potentially public disclosure requirements. This portal provides the mandatory human review and approval layer: no return is submitted until a Finance officer has reviewed the assembled data, confirmed correctness, and recorded a signed approval.

Submission calendar

The landing page aggregates all regulatory returns across all submission modules. Each return shows its return code, jurisdiction, period end date, due date, current status (ASSEMBLING / VALIDATED / AWAITING_APPROVAL / APPROVED / SUBMITTED / ACKED / OVERDUE), and the name of the officer who last acted on it. The calendar refreshes within 1 hour of any status change. Overdue returns are highlighted in red; returns due within 7 days are amber.

Return viewer and approval workflow

Clicking any return in the calendar opens the cell-level return viewer. The viewer shows: - Every line item code, value, and the validation status (pass / fail / warning) - Source lineage for each cell: which upstream view and model_run_id produced it - Validation error details if any validation rules failed - Prior period values for comparison

At the bottom of the viewer, an authorised Finance officer sees an approval form. They can: - Approve — records a row in REGULATORY.RETURN_APPROVALS with their staff_id (from their Snowflake session), timestamp, run_id, return_code, jurisdiction, and a mandatory sign-off comment. This releases the submission orchestrator in MOD-036 to post to the regulator API. - Reject — records a rejection with reason; the assembly pipeline must re-run before approval can be granted again.

The approving officer must be a different Snowflake user from the system account that assembled the return (four-eyes principle, GOV-002). The approval is immutable once written — NFR-024 applies to RETURN_APPROVALS.

Historical submissions log

A separate page shows every submission ever made across all regulatory modules, with regulator acknowledgement status, content hash, submission reference, and 7-year retention indicator.

Snowflake objects

This module owns no computation tables — it is a pure read-and-write Streamlit layer. It writes only to REGULATORY.RETURN_APPROVALS (owned by MOD-036, cross-schema grant required: GRANT INSERT ON REGULATORY.RETURN_APPROVALS TO regulatory_submissions_portal_role).

The Streamlit app is deployed as REGULATORY.STREAMLIT_SUBMISSIONS_PORTAL in MOD-036's REGULATORY schema, using MOD-036's Snowflake role for read access to all REGULATORY objects.

Role-based access

Access to the portal is restricted to staff with the finance.regulatory_reporting or compliance.officer Cognito group (forwarded from the RBNZ/APRA staff identity pool). The approval action is additionally restricted to finance.senior_officer or compliance.head — junior reporting analysts can view but not approve.

Dependency on MOD-036

MOD-036 must be built and the REGULATORY schema must exist before this module can be deployed. MOD-037, MOD-057, and MOD-060 are optional — the portal degrades gracefully, showing only the return modules that have been deployed.

Policies satisfied:

Policy Mode Description
REP-005 — Data Quality & Assurance Policy GATE No regulatory return may be submitted to RBNZ or APRA without passing validation (REP-005 data quality gate) AND receiving explicit written approval from an authorised Finance officer recorded in REGULATORY.RETURN_APPROVALS — the submission orchestrator enforces both gates unconditionally.
REP-001 — Regulatory Reporting Policy AUTO The unified submission calendar aggregates all return statuses across MOD-036, MOD-037, MOD-057, and MOD-060, updated within 1 hour of any status change, satisfying the automated regulatory reporting visibility requirement.
REP-002 — Prudential Reporting Policy LOG Every return approval records the approving officer identity, timestamp, run_id, return_code, and sign-off reason in REGULATORY.RETURN_APPROVALS — immutable per NFR-024, providing the prudential reporting audit trail required by REP-002.
GOV-002 — Risk Appetite Statement Policy LOG All portal access events (view, approve, reject) are logged with staff_id, timestamp, return_code, and run_id, satisfying the Risk Appetite Statement governance audit requirement for regulatory submission actions.

MOD-171 — Risk Intelligence Dashboard

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

The Risk Intelligence Dashboard is the primary human interface for the CRO, CFO, Treasurer, and Risk team to monitor the bank's quantitative risk position across capital, liquidity, interest rate risk, stress testing, and model outputs. It is a Snowflake Streamlit application that reads exclusively from the published views of the SD06 risk modules — all computation stays in its owning module; this dashboard is a read-only aggregation layer.

Why it exists

Every SD06 risk module (MOD-032, MOD-033, MOD-035, MOD-039, MOD-086, MOD-098) builds excellent Snowflake models and published views. But the CRO cannot be expected to open Snowsight and write SQL to read LCR numbers before their 8am risk committee meeting. This module converts those views into a board-ready real-time dashboard. The operating principle: every figure the CRO sees in this dashboard is the same figure the models compute — no manual extracts, no spreadsheet intermediaries.

RAF summary

The landing page is the Risk Appetite Framework summary. Each configured RAF indicator appears as a gauge or traffic light:

  • CET1 ratio — from MOD-033 V_CAPITAL_CURRENT; threshold from RAF config table
  • LCR — from MOD-032 V_LCR_CURRENT
  • NSFR — from MOD-032 V_NSFR_CURRENT
  • EVE sensitivity (maximum shock scenario) — from MOD-035 published view
  • Stress test capital headroom — from MOD-034 when deployed
  • Related party exposure % — from MOD-147 when deployed
  • Customer risk concentration — from MOD-039 high-risk segment share

A breach (indicator outside RAF threshold) highlights the card red and was already alerted by the owning module's Snowflake Alert. The dashboard makes the breach visible to the human at the same time.

Each indicator shows a 90-day sparkline. The board risk report is auto-generated monthly from this page's data via a scheduled Snowflake Task.

Capital deep-dive

Sourced from MOD-033. Shows CET1, Tier 1, Total Capital, and RWA broken down by exposure class (corporate, retail, sovereign, securitisation). Period-over-period comparison. Each row links back to the V_CAPITAL_BY_PORTFOLIO row via model_run_id — full lineage to individual loan positions if needed.

Liquidity dashboard

Sourced from MOD-032. Shows LCR (HQLA, total net cash outflows, ratio) and NSFR (available stable funding, required stable funding, ratio) for the current day and prior 30 days. Intraday exposure overlay when MOD-032's intraday extension is deployed.

IRRBB sensitivity dashboard

Sourced from MOD-035. Shows EVE and NII sensitivity under each standard interest rate shock scenario (+200bp, -200bp, +100bp, -100bp, twist, parallel). Scenario comparison chart. Period delta vs. prior month-end. Limit headroom for each scenario.

Risk metrics overview

A single page showing three panels: (1) customer risk score distribution histogram from MOD-039; (2) FTP rate table by product / tenor from MOD-086; (3) cost allocation pie by system domain from MOD-098. Each panel has a "drill down" link to the owning module's own Streamlit page for further detail.

Architecture

Pure Streamlit — no DCM tables, no Lambdas, no dbt models. The module deploys a single Streamlit object (RISK_INTELLIGENCE.STREAMLIT_RISK_DASHBOARD) into a new RISK_INTELLIGENCE schema in BANK_{ENV}_RISK. Cross-schema SELECT grants on each source module's published views are required (same pattern as ADR-064 published view contracts applied to Snowflake cross-schema reads). Each source module's migration must grant SELECT on its published views to RISK_INTELLIGENCE_ROLE.

SSM outputs: /bank/{env}/risk-platform/risk-intelligence/streamlit-url — the Streamlit app URL for use in the portal navigation and internal dashboards.

Policies satisfied:

Policy Mode Description
GOV-002 — Risk Appetite Statement Policy CALC The RAF summary page continuously computes all configured risk appetite indicators (CET1, LCR, NSFR, EVE, stress headroom, related party exposure) from SD06 published views and displays them against board-approved thresholds — the dashboard IS the Risk Appetite Framework reporting required by GOV-002.
REP-002 — Prudential Reporting Policy LOG Every metric shown in the dashboard is sourced from a published view with full model_run_id lineage, satisfying the prudential reporting data lineage LOG requirement — no figure is computed outside its owning module.
DT-005 — Model Risk Management Policy LOG Model performance indicators (accuracy, PSI, drift alerts) for all deployed SD06 quantitative models are surfaced in the risk metrics overview, maintaining the model inventory and monitoring visibility required by DT-005.

MOD-172 — Operations & Model Intelligence Dashboard

System: SD06 | Repo: bank-risk-platform | Build status: Deployed | Deployed: Yes

The Operations & Model Intelligence Dashboard is the human interface for the COO, Finance team, and Data Platform team to monitor the health of the bank's operational data models, statutory financial outputs, model accuracy, and cost attribution. It is a Snowflake Streamlit application that reads from the published views of MOD-038, MOD-039, MOD-040, MOD-041, MOD-080, and MOD-098, and provides a navigation link to the MOD-056 Compliance Visibility Engine.

Why it exists

The SD06 operational modules produce rich outputs — DQ scores, churn predictions, transaction categorisations, period financial statements, cost allocations — that the teams responsible for them have no human interface to review. A data quality break in MOD-038 fires an alert, but the DQ analyst has to log into Snowsight to understand which domain is affected and by how much. A quarterly statutory close in MOD-080 produces a trial balance that nobody can look at without writing SQL. This module converts those published views into the operational dashboards the teams need.

DQ scorecard

The landing page. Shows MOD-038's current DQ score by system domain in a heat-map table. For each domain: break count, break rate (% of rows), open-break trend over 30 days, and last-refreshed timestamp. Clicking any domain opens the detailed break list from MOD-038's published views. Domains with active breaks are sorted to the top. The scorecard gives the data engineering team an instant answer to "what's broken today?" without navigating Snowsight.

Statutory financials viewer

Sourced from MOD-080. Shows the most recently closed period's profit and loss, balance sheet, and cash flow statement in a formatted table view — the same figures that flow to the ERP. Navigation by period (prior quarters accessible). Each line item links to the trial balance rows that compose it via model_run_id. Read-only — no edits; the only source of truth for the financial statements is MOD-080.

Model performance dashboard

Shows accuracy and stability metrics for the three primary operational models:

  • MOD-039 Customer risk score — distribution of scores across the portfolio; PSI vs. prior month; any drift alert status
  • MOD-040 Churn & health score — model accuracy (AUC) on last validation run; PSI; top features driving churn predictions
  • MOD-041 Categorisation & merchant enrichment — categorisation coverage rate (% of transactions with a non-null category); merchant-enrichment match rate; accuracy on held-out validation set

Any model with PSI above threshold or accuracy below the configured floor is flagged red. The Data Science team reviews the flag before the model is re-trained. This page is the primary DT-005 model monitoring surface.

Cost attribution view

Sourced from MOD-098. Shows total cost allocated by system domain (SD01–SD08) and by product for the current month and trailing 12 months. Period-over-period comparison. Drill-down to individual cost driver categories. Used by Finance for management accounting and product profitability analysis.

A clearly labelled section at the bottom of the navigation sidebar links to the MOD-056 Compliance Visibility Engine Streamlit. When the user clicks through, they move from the operational view into the full regulation → policy → module → runtime evidence compliance map. This satisfies FR-818 without duplicating MOD-056's functionality.

Architecture

Pure Streamlit — no DCM tables, no Lambdas, no dbt models. Deployed as OPERATIONS.STREAMLIT_OPS_INTELLIGENCE in a new OPERATIONS schema in BANK_{ENV}_RISK. Cross-schema SELECT grants on each source module's published views required — each source module migration grants SELECT on its published views to OPERATIONS_ROLE.

SSM output: /bank/{env}/risk-platform/operations-intelligence/streamlit-url.

Policies satisfied:

Policy Mode Description
DT-005 — Model Risk Management Policy LOG Model performance metrics (accuracy, PSI, drift alerts) for all deployed SD06 operational models are surfaced and logged per model inventory requirements — any model showing PSI above threshold or accuracy degradation is flagged for review.
OPS-003 — Incident Management Policy AUTO Data quality breaks from MOD-038 are surfaced as scorecard items in the dashboard; the DQ scorecard acts as the first-line operational health indicator for the data platform, complementing MOD-150 incident management.
REP-002 — Prudential Reporting Policy LOG Statutory financials displayed in the viewer are sourced directly from MOD-080 published views with full model_run_id lineage — no figures are recomputed outside the owning module, satisfying REP-002 prudential reporting lineage LOG.

MOD-173 — Model risk register & inventory

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Central Snowflake-native model register covering every model-bearing module on the platform. Serves as the authoritative inventory for the platform's own model-risk governance, and pre-populates the register fields that APS 113 and the RBNZ internal models compendium require — so customers can pull a ready-made register entry rather than constructing one from scratch.

Purpose

Every institution that deploys a platform model needs a model register entry for that model. APS 113 mandates specific fields (type, scope, IRB asset class, materiality/exposure, model owner, implementation date, validation rating, last/next validation dates). The RBNZ requires an agreed internal models compendium for IRB-accredited deposit takers. Building a register entry from scratch requires understanding the platform model in detail — the platform can supply that understanding directly.

MOD-173 maintains one canonical register entry per model-bearing module, kept current as the model evolves, and exposes it via a read-only API so each customer's own model inventory system can pull the record directly.

What it stores

For each model-bearing module (MOD-028, 030, 031, 033, 034, 035, 017, 023, 039, 055, 041):

  • Model identity: module ID, title, version, effective date, SHA of the deployed model artefact
  • Classification: model type, scope, applicable portfolio / IRB asset class, risk tier (1 / 2 / 3 per DT-013)
  • Materiality: total exposure and percentage of portfolio (customer-parameterised at deploy time)
  • Ownership: model owner, validation function, independent reviewer
  • Validation status: current validation rating, date of last validation, date of next scheduled validation
  • Evidence pack reference: S3 path to the current evidence pack artefacts
  • Pre-validation report: reference to the latest third-party pre-validation report
  • Regulatory approval status: APRA approval date (where applicable); RBNZ approval status and compendium reference (where applicable)
  • Change history: link to MOD-175 change-control records for this model

Deployment gate integration

The deployment pipeline gate for any model-bearing module checks that a register entry exists and is current before allowing promotion to production. A model module without a register entry will not deploy. This enforces the DT-013 definition-of-done at the infrastructure level.

Compliance visibility integration

If MOD-056 (compliance visibility engine) is deployed, it can read the register to surface approaching validation deadlines as obligation events — routing to the customer's compliance calendar automatically.

Policies satisfied:

Policy Mode Description
DT-013 — Model Validation & Audit Policy GATE A model-bearing module cannot be marked Deployed without a corresponding register entry; the deployment pipeline gate enforces this check before promotion to production.
DT-005 — Model Risk Management Policy LOG Central model inventory maintains the register required by the Model Risk Management Policy, covering all platform model-bearing modules with APS 113 and RBNZ compendium fields.

MOD-174 — Model performance monitoring & drift detection

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Snowflake-native ongoing-monitoring platform that runs automatically in every customer deployment. Generates the monitoring evidence the customer's validation function needs — drift detection, population-stability indices, calibration tracking, back-test refresh, and performance dashboards — without any manual engagement. Turns a recurring customer cost (typically a quarterly or annual model monitoring engagement) into an automatic platform feature.

Why ongoing monitoring matters

SR 11-7 and PRA SS1/23 both treat ongoing monitoring as a core validation pillar. APRA's APS 113 requires evidence that models continue to perform as intended, and IFRS 9's ECL models attract scrutiny over whether staging logic and PD/LGD estimates remain appropriate as the economic cycle evolves. Without automated monitoring, smaller institutions typically rely on point-in-time annual validation engagements — which means model drift can go undetected for months.

The platform is in a unique position: it runs the same models across many customers and can aggregate performance evidence that no single institution could generate independently.

What it monitors per model

Drift detection. Population stability index (PSI) on input features; characteristic stability index (CSI) per predictor. Alerts generated when PSI exceeds configurable thresholds (default warn >0.1, critical >0.2).

Calibration tracking. For PD models: Hosmer-Lemeshow test, Brier score, binomial test on actual default rates vs predicted. For ECL models: monthly comparison of provision outcomes against ECL estimates. For scoring models: Gini coefficient, KS statistic, AUC trend over rolling 12-month window.

Back-test refresh. Automated quarterly back-test using outcomes data from MOD-048 (system decision log) and MOD-047 (agent action logger) where deployed. Results appended to the evidence pack in MOD-173.

Population monitoring. Input distribution monitoring; missing value rates; out-of-range flags. Especially important for AI/ML models (MOD-017, MOD-023, MOD-039, MOD-055) where data distribution shift is the primary failure mode.

Dashboards

Snowflake-native Streamlit dashboards per model tier. Tier 1 models (MOD-028, 030, 031, 033) have a full performance dashboard accessible to the customer's validation function and internal audit. All models have a model-health summary card displayed on the SD06 risk-platform overview.

Evidence pack integration

All monitoring outputs are automatically written to the model's evidence pack record in MOD-173, timestamped and immutable. The customer's validator can pull the full monitoring history at any time. This is the artefact that closes the "ongoing monitoring" criterion in a model validation — supplied automatically, not built manually.

Policies satisfied:

Policy Mode Description
DT-013 — Model Validation & Audit Policy AUTO Drift detection, population-stability indices, calibration tracking and back-test refresh run automatically on schedule for every deployed model — no manual trigger required; satisfies the ongoing-monitoring pillar of DT-013.
DT-005 — Model Risk Management Policy AUTO Continuous model performance monitoring satisfies the ongoing-monitoring pillar of the Model Risk Management Policy without requiring manual customer engagement.

MOD-175 — Model change control & re-approval workflow

System: SD06 | Repo: bank-risk-platform | Build status: Not started | Deployed: No

Workflow engine that wraps every platform model change in a structured change-control record and enforces the approval gates required before the changed model reaches production. For NZ capital models, the gate is extended to require RBNZ re-approval artefacts before the deployment lock is released. This module is the direct technical response to the regulatory risk illustrated by the Westpac NZ case (2016): operating an unapproved model change is a breach of the conditions of registration.

Why this is non-negotiable

The RBNZ framework is explicit: a change to an approved capital model without first obtaining RBNZ approval breaches the bank's conditions of registration. This is not a risk-management deficiency — it is a licence breach. The Westpac NZ case involved a risk-weighted-asset impact exceeding NZD 1 billion and triggered a mandatory section 95 independent review. The platform must never put a NZ customer in that position, regardless of how incremental a model change appears.

APRA's APS 113 similarly requires notification or approval for material model changes before they go live.

Change-control record

Every model change — whether a parameter recalibration, a methodology update, or a code fix — generates a change-control record containing:

  • Change description and classification (minor / material / major)
  • Model register reference (MOD-173 entry for the affected model)
  • Re-validation evidence: what testing was performed, what changed in performance
  • Impact assessment: expected change in model outputs across representative portfolios
  • Approval requirements: for NZ capital models, the RBNZ re-approval pathway is surfaced explicitly with the required artefacts pre-populated
  • Sign-off chain: model owner → validation function → (for Tier 1 models) independent reviewer → deployment gate

RBNZ re-approval pathway

For capital model changes affecting NZ deployments (primarily MOD-033):

  1. Change-control record is created with classification = material or major
  2. MOD-175 generates the RBNZ submission artefact package (change description, impact analysis, re-validation evidence) in the format the RBNZ expects
  3. The deployment gate in the NZ customer's pipeline is locked
  4. The customer submits to RBNZ and records the approval reference in MOD-175
  5. Only after the approval reference is recorded does the gate open and the deployment proceed

This workflow is integrated with MOD-168 (maker-checker enforcement engine) which provides the multi-party sign-off mechanism. No single actor can release the gate unilaterally.

What triggers a material change classification

A change is classified as material if it results in a change of more than 2% in total model-estimated RWA, total ECL provision, aggregate PD, or model Gini coefficient — configurable per model. Minor changes (e.g. bug fixes with no measurable output impact) follow an expedited track with reduced sign-off requirements but still generate an audit record.

Audit trail

All change-control records are immutable once created. The audit trail of every model change — who proposed it, what was tested, who approved it, when it went live — is retained for the life of the model. This is the artefact internal audit needs for the annual independent review under APS 113.

Policies satisfied:

Policy Mode Description
DT-013 — Model Validation & Audit Policy GATE No model change reaches production without a completed change-control record; for NZ capital models the gate requires RBNZ re-approval artefacts to be present before the deployment lock is released — directly preventing a conditions-of-registration breach of the Westpac NZ type.
DT-005 — Model Risk Management Policy GATE Change-control enforcement before model update deployment satisfies the model-change management requirement of the Model Risk Management Policy.

SD07 — Data Platform & Governance Infrastructure

Repo: bank-platform | Business domain: BD09 | Tech owner: Platform Engineering | Build status: Not started

The CDC pipeline (Neon to S3 Iceberg), EventBridge domain event buses, IAM, secrets management, and data governance tooling that underpins all other systems.

Modules

ID Name Status ADR
MOD-042 CDC pipeline — Neon logical replication to S3 Iceberg Not started ADR-001, ADR-003
MOD-043 EventBridge domain event governance Not started ADR-029
MOD-044 JWT role-based access control Not started ADR-004
MOD-045 Secrets & key management Not started
MOD-046 Privileged access management (PAM) Not started
MOD-047 Agent action logger Not started ADR-004
MOD-048 System decision log Not started ADR-001

For full module specifications and acceptance criteria, see module specifications.

Architecture

See ADR-003 for the CDC pipeline decision, ADR-029 for domain event routing, and ADR-004 for the JWT RBAC and agent access control pattern.

Critical constraints

  1. MOD-042 CDC pipeline must deliver Neon Postgres changes to Snowflake within 5 minutes p99 (NFR-015). Monitoring must alert if the CDC Lambda fails for more than 30 continuous hours.
  2. MOD-044 JWT RBAC is a hard GATE — no system or agent may access data without a valid scoped token.
  3. MOD-047 must log every agent action with actor, target, action type, and timestamp — no exceptions.
  4. Secrets must never appear in logs, environment variables visible to application code, or version control.

Modules in SD07


MOD-042 — CDC pipeline — Neon logical replication to S3 Iceberg

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Scheduled Lambda (60-second interval via EventBridge Scheduler) connects directly to each Neon domain database, reads committed WAL changes via pg_logical_slot_get_changes(), and publishes records to Kinesis Firehose. Firehose writes Apache Iceberg files to S3, catalogued in AWS Glue Data Catalog. Snowflake reads via External Iceberg Tables — zero-copy, no Snowpipe ingestion cost.

One replication slot per domain database. Last-acknowledged LSN persisted in S3 alongside the data files. Monitoring alerts if the Lambda fails for more than 30 continuous hours (Neon drops inactive slots at ~40 hours).

See ADR-003.

Policies satisfied:

Policy Mode Description
DT-004 — Data Governance Policy AUTO All operational data changes flow through a single governed CDC pipeline — no shadow extracts or parallel data taps permitted
REP-005 — Data Quality & Assurance Policy AUTO Regulatory data sourced from the same Iceberg snapshots as all consumers — no divergent copies or selective replication
AML-005 — Transaction Monitoring Policy AUTO Transaction events available to the AML monitoring engine within 5 minutes of posting via S3 Iceberg External Table

MOD-043 — EventBridge domain event governance

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Provisions and governs the eight custom EventBridge event buses (one per system domain: bank.core, bank.kyc, bank-aml, bank.payments, bank.credit, bank.risk, bank.platform, bank.app). Manages IAM resource policies, EventBridge Schema Registry schemas, and the SQS dead letter queues attached to every rule target.

Schema Registry enforces backward-compatible event contracts between producing and consuming Lambdas. Breaking changes require a new event type — schema mutation is not permitted. Operations monitoring alerts on DLQ depth > 0 across all buses.

See ADR-029.

Policies satisfied:

Policy Mode Description
DT-004 — Data Governance Policy AUTO Domain event buses enforce ownership boundaries — cross-domain subscriptions require an explicit published contract
DT-001 — Information Security Policy AUTO EventBridge bus access governed by IAM resource policies — only authorised Lambda functions may publish or subscribe
PRI-001 — Privacy Policy AUTO Event payloads must not contain PII — personal data referenced by entity ID only, retrieved from the authoritative domain store
PRI-003 — Personal Information Retention & Destruction Policy AUTO DLQ messages capped at 14-day TTL — no event payload retained beyond the operational resolution window

MOD-044 — JWT role-based access control

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

All API calls authenticated via JWT containing role claims. Gateway enforces scope at API level. See ADR-004.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE Least-privilege enforced at API gateway — no client-side security reliance
GOV-007 — Conflicts of Interest Policy AUTO Role separation enforced — no single user can hold conflicting roles
GOV-006 — Internal Audit Policy LOG All authenticated API calls logged with user ID, role, endpoint, and timestamp

MOD-045 — Secrets & key management

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

All secrets managed in AWS KMS / HashiCorp Vault. No secrets in code or config files. Automatic rotation on schedule.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy AUTO Secrets cannot be extracted by developers — vaulted and access-controlled
DT-002 — Cybersecurity Policy AUTO Key rotation automated — no reliance on manual rotation schedule
AML-007 — Sanctions Screening Policy AUTO Sanctions list decryption keys managed centrally — no offline copy possible

MOD-046 — Privileged access management (PAM)

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Production database and infrastructure access requires time-limited, approved, logged sessions. No standing access to production for any engineer.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE No standing production access — every session is approved, time-limited, and logged
GOV-006 — Internal Audit Policy LOG All production access sessions available to audit — who accessed what and when
DT-002 — Cybersecurity Policy LOG Insider threat risk reduced — no engineer can access production data without an auditable session

MOD-047 — Agent action logger

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Every back office action written to immutable audit log with agent ID, timestamp, before/after state, and justification. Append-only.

Policies satisfied:

Policy Mode Description
GOV-006 — Internal Audit Policy LOG Internal audit can reconstruct any agent action — no action is unlogged
AML-001 — AML/CFT Programme Policy LOG AML programme execution evidenced by audit log — regulator can inspect any decision
GOV-005 — Financial Accountability Regime (FAR) Policy LOG FAR accountable person accountability evidenced by action logs under their remit
CON-002 — Complaints & Internal Dispute Resolution Policy LOG Complaint handling actions logged — IDR process is fully auditable

MOD-048 — System decision log

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Automated system decisions written to audit log alongside model version and feature inputs that drove the decision.

Policies satisfied:

Policy Mode Description
DT-009 — AI & algorithm policy LOG AI/ML decisions are explainable — inputs and model version logged against every automated decision
CRE-003 — Credit Decisioning & Scorecard Policy LOG Every credit decision auditable — customer can receive explanation, regulator can inspect
AML-006 — Suspicious Activity Reporting Policy LOG AML alert dismissals logged with analyst ID and reasoning — not a silent discard
GOV-006 — Internal Audit Policy LOG Automated decisions subject to audit — third line can sample and QA system decisions

MOD-062 — Workflow orchestration engine

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

The workflow orchestration engine is the runtime that executes multi-step user journeys as defined sequences of steps, decisions, and integrations. It replaces hard-coded flow logic with a configurable graph of states, transitions, and actions — allowing onboarding, credit application, dispute, and trade finance workflows to be defined once and operated consistently across channels.

The engine calls into other modules (decisioning, KYC, validation, payment) at each step, handles retries and timeouts, and evaluates branching conditions using the output of each call. Customers see progress indicators and can resume interrupted journeys; operations staff can see the full journey state, identify where a customer is stuck, and intervene at any step.

Designed as a durable execution engine: every state transition is persisted before being acted on, ensuring no journey is lost to an infrastructure failure and no step is executed twice.

FR-295 / FR-740 scope split

FR-295 covers the Step Functions task-token approval pause/resume mechanism (durable approval, authorised approvers only). FR-740 covers workflow versioning at the execution layer (pinning in-flight executions to a specific workflow definition version). FR-740 is currently deferred; the workflow_version tag stamped on each state machine is a traceability marker only — there is no version comparison or in-flight execution pinning.

Policies satisfied:

(No policies assigned)


MOD-063 — Notification orchestration

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Notification orchestration is the decisioning layer between platform events and customer communications. Rather than individual modules sending their own messages, all outbound communications are routed through this module, which applies preference rules, channel selection logic, regulatory timing constraints, and deduplication before dispatch.

The module subscribes to domain events from the EventBridge governance layer (MOD-043) and to workflow transition events from the orchestration engine (MOD-062). For each event it evaluates whether a notification is required, what content to use, and which channel to use given the customer's stored preferences (CAP-104). Delivery is delegated to the appropriate channel infrastructure (push, SMS, SMTP) but the decision, content, and outcome are always logged here.

Provides the audit trail required for regulatory disclosure obligations — compliance can demonstrate that a required disclosure was sent, when, to which address, and whether it was delivered.

Policies satisfied:

Policy Mode Description
CON-001 — Customer Fairness & Conduct Policy AUTO Ensures all required regulatory disclosures and event-driven communications are sent to the customer at the correct time.
GOV-003 — Three Lines of Defence Policy LOG All customer communications are logged with content, channel, timestamp, and delivery status for audit purposes.

MOD-075 — Internal API gateway

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

The internal API gateway is the single entry point for all service-to-service and app-to-backend communication within the platform. Every API call from the customer app, the back-office app, and inter-service integrations passes through this gateway, which handles TLS termination, service authentication, rate limiting, request routing, API version negotiation, and request logging before forwarding to the target service.

Unlike the Open Banking gateway (MOD-061) — which handles external third-party CDR access — and the JWT RBAC module (MOD-044) — which handles token validation — the internal gateway owns the routing and reliability layer: circuit breaking, retry with backoff, timeout enforcement, and canary routing for deployments. It is the chokepoint that prevents any single misbehaving service from degrading the platform, and the place where cross-cutting concerns (logging, tracing header injection, correlation ID stamping) are applied uniformly.

API versioning is managed here: multiple versions of a service can be live simultaneously, with the gateway routing requests to the correct version based on the Accept or API-Version header. Deprecation notices are injected as response headers so consumers can track sunset timelines without consulting documentation.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE All service-to-service traffic passes through TLS-terminated endpoints with mutual authentication — no plaintext internal API calls are permitted.
DT-002 — Cybersecurity Policy GATE Rate limiting and request signing enforce that only registered, authenticated services can call platform APIs — unauthenticated requests are rejected at the gateway.

MOD-076 — Observability platform

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

The observability platform provides the platform engineering team with full visibility into the runtime behaviour of every service: distributed traces for request-level debugging, time-series metrics for system health and SLO monitoring, structured logs for error investigation, and alerting rules that page the on-call engineer when something needs immediate attention.

All services emit traces using an OpenTelemetry SDK. The observability platform collects, correlates, and stores these traces, allowing any individual API call to be followed end-to-end across all services it touched — including the time spent, any errors encountered, and the database queries executed at each step. This is the primary tool for diagnosing latency regressions and cascading failures in production.

Metrics cover both system-level indicators (CPU, memory, network, queue depth) and business-level indicators (payment processing rate, KYC decision latency, fraud score distribution). SLO dashboards track the bank's reliability commitments over rolling windows. Alerting routes pages to the on-call engineer via the configured channel (PagerDuty, Slack) based on severity rules. Logs are centralised with full-text search and retained for 90 days in hot storage and 7 years in cold storage for regulatory purposes.

Policies satisfied:

Policy Mode Description
GOV-006 — Internal Audit Policy LOG Platform-level system events, errors, and performance anomalies are captured in the observability store — available for internal audit review.
DT-004 — Data Governance Policy ALERT Data quality anomalies detected by pipeline monitors are surfaced as observability alerts — the DT-004 obligation to detect and respond is operationalised here.

MOD-079 — Snowflake decision publication service

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

The Snowflake decision publication service is the operational apply service defined in ADR-036. It is the only governed path by which Snowflake-generated decisions cross the Snowflake → Neon boundary and take operational effect.

Purpose

Snowflake produces decisions — onboarding outcomes, CDD tier assignments, fraud actions, credit pre-approvals, AML case escalations. These decisions must be applied to Neon's operational tables to change how the bank treats a customer. Without a governed publication path, decisions either do not take effect or require ad-hoc database writes that undermine auditability.

This module receives published decision payloads on the decision_inbox.decision_result_inbox table in Neon, validates the contract version and schema, deduplicates on the idempotency_key, and applies the decision to the correct operational table. Every applied decision is recorded in decision_delivery_log with its source computation, contract version, policy references, and effective timestamp.

What it does

  • Receives versioned decision payloads from Snowflake's decision_curated.decision_result view via the ADR-036 publication contract
  • Validates schema version, entity type, and mandatory fields before applying anything
  • Deduplicates on decision_id + idempotency_key — safe to replay without double-applying
  • Routes each decision type to its target operational table: onboarding → customers.onboarding_status; CDD tier → customers.cdd_tier; fraud action → accounts.status; credit pre-approval → credit_decisions; AML escalation → aml_cases.status
  • Logs every outcome — applied, duplicate, rejected — to decision_delivery_log

What it does not do

This module does not write Tier 3 reporting data. It does not receive raw Snowflake features or intermediate model scores. It does not accept direct database writes from Snowflake — all traffic arrives via the ADR-036 decision inbox contract. For the data tier classification see ADR-038.

Failure handling

If the apply step fails after successful inbox receipt, the record remains in the inbox with a failed status and is retried. Dead-letter records older than 24 hours are escalated via the operations work queue (MOD-064). Snowflake does not retry — the inbox record is the durable state.

Policies satisfied:

Policy Mode Description
GOV-006 — Internal Audit Policy LOG Every applied decision is recorded with its source Snowflake computation, contract version, policy reference, and operator identity — immutable audit trail.
DT-001 — Information Security Policy GATE Only schema-version-validated, policy-sanctioned outcomes cross the Snowflake → Neon boundary — raw features and intermediate scores never leave Snowflake.

MOD-087 — Transaction enrichment engine

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

What it does

MOD-087 is the transaction enrichment foundation for the Expense Intelligence Platform. It receives raw transaction events from the core banking ledger and produces enriched transaction records with normalised merchant name, merchant logo, industry code (MCC), geolocation, and an initial spend category signal.

Enriched records are published to the internal event bus and consumed by MOD-088 (expense classification engine), MOD-089 (geo-spatial processor), MOD-091 (receipt processor), and the customer app layer. Every downstream module in the Expense Intelligence Platform depends on the output of MOD-087 — raw transaction data is not consumed directly.

Input schema (v1)

MOD-087 subscribes to bank.core.posting_completed (schema 1.1.0) via a cross-bus EventBridge rule provisioned by MOD-103. The event does not carry a raw_merchant_name field or a party_id.

posting_type filter (hard gate). MOD-087 processes only postings where posting_type = 'PAYMENT' (card and EFTPOS transactions). All other types (ACCRUAL, FX_CONVERSION, ADJUSTMENT, REVERSAL, PROVISION) are silently skipped — the handler returns without emitting bank.platform.transaction_enriched. Enriching a fee or accrual posting with a merchant name would be a data quality defect.

raw_merchant_name — v1 simplification (Ruling 4A). MOD-087 retrieves narrative from accounts.postings via a cross-schema SELECT and uses it as raw_merchant_name in both the dictionary lookup and the output event. Card-acquirer narratives ("Caltex Newtown", "PAK'NSAVE 0017") are the raw merchant name in practice; the dictionary normaliser's purpose is resolving variant forms. Documented as a v1 simplification. v2 path: extend posting_completed to carry a dedicated raw_merchant_name field (Option B in the original ruling), or let the external API (ExternalApiEnrichmentProvider) accept narrative + account_context — either is mechanical given the EnrichmentProvider interface.

party_id. MOD-087 retrieves party_id via a cross-schema SELECT on accounts.account_party_relationships WHERE account_id = ? AND relationship_type = 'PRIMARY'. This is the same ADR-064 published view contract pattern used across other cross-schema reads. A platform_enrichment_ro read grant on both accounts.postings and accounts.account_party_relationships must be provisioned by MOD-103 before MOD-087 can be deployed.

Merchant normalisation

Raw acquirer merchant names are often truncated, location-coded, or inconsistent across terminals. MOD-087 resolves these to a canonical merchant identity using an internal normalisation dictionary (v1) and an external merchant enrichment API (v2). Once resolved, a merchant record is cached in platform.enrichment_merchants so future transactions at the same merchant incur no enrichment latency.

v1 scope: Built-in dictionary (~50 NZ/AU merchant patterns covering canonical name + MCC). No external API call. logo_url, lat, and lng are nullable — populated once external API integration lands. Enrichment source is DICTIONARY or MCC_INFERENCE only in v1; MANUAL for seed rows.

Enrichment provider interface: All enrichment logic runs behind an EnrichmentProvider interface (src/shared/enrichment-provider.ts). v1 ships DictionaryEnrichmentProvider. v2 adds an ExternalApiEnrichmentProvider (Mastercard Merchant Insights or equivalent, routed via MOD-157 in dev) without modifying the Lambda handler. This is required for FR-762 architectural compliance.

initial_category is out of scope. MOD-088 (expense classification engine) owns spend categorisation. MOD-087 sticks to merchant identity and geolocation per FR-762 and FR-763.

Postgres table

platform.enrichment_merchants — merchant identity cache. Owned by MOD-087. See SD07 data model for full schema.

Published event

bank.platform.transaction_enriched on the bank-platform bus. Published after each enrichment. See the event catalogue for the full schema. Consumers: MOD-088 (expense classification), MOD-089 (geo-spatial processor), MOD-091 (receipt processor), customer app layer.

Compliance

PRI-001 LOG — every enrichment writes a structured audit entry recording posting_id, raw_merchant_name, enrichment_source, and MCC so data-minimisation audits can verify only the fields required for classification are retained.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Enrichment processing is logged with source signals so data minimisation audits can verify that only the fields required for classification are retained.

MOD-093 — Accounting mapper

System: SD07 | Repo: bank-platform | Build status: Not started | Deployed: No

What it does

MOD-093 is the accounting mapper. It handles all outbound integration to accounting platforms (Xero, MYOB) and direct lodgement channels (IRD myIR API, ATO SBR2), transforming the bank's classified and tax-logic-processed transaction data into the format each destination expects.

Xero integration

Xero API (OAuth 2.0) integration supports: - Push pre-classified transactions as bank transactions with account code, tax code, and tracking category pre-populated - Attach receipt files from MOD-091 as Xero file attachments - Create draft GST returns for customer review before filing

The design intent is that Xero becomes a review and accountant-access layer, not a classification layer. The bank does the work; Xero receives clean output.

MYOB integration

MYOB AccountRight and Essentials APIs provide equivalent functionality for the AU market where MYOB has stronger SME penetration.

IRD direct lodgement (Phase 4)

Direct lodgement via IRD Gateway Services (myIR API) for GST returns. Removes the accounting platform entirely for simple cases — a sole trader or property investor with straightforward accounts can file GST directly from the bank app.

ATO SBR2 (Phase 4)

Standard Business Reporting v2 for AU BAS lodgement directly to the ATO.

Accountant export

CSV, ICFM, and Xero-compatible export formats for customers who share data with an accountant. The export includes the full audit trail of classification decisions from MOD-088 and MOD-090.

Design phase

This module is in design. Build begins in Phase 3 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Classified transaction data pushed to Xero and MYOB constitutes personal financial data; all outbound data transfers are logged for data minimisation audits and individual access requests.

MOD-097 — Usage event collector

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-097 is the instrumentation layer for the entire platform. It collects a structured usage event every time any module performs a billable operation, and streams those events into the Snowflake metering schema where they become the raw material for cost attribution, billing, and unit economics analysis.

The module introduces no changes to individual module code. Instrumentation is applied as a shared Lambda layer attached to all tenant-context function invocations, and via EventBridge rule patterns that capture billable operations by their existing event shapes.

What counts as a billable usage event

Event type Examples Unit
API_CALL Customer API request through API Gateway Per call
ML_INFERENCE Fraud score, credit score, categorisation, enrichment Per inference
SNOWFLAKE_QUERY Risk model run, regulatory calculation, analytics query Per credit consumed
ENRICHMENT_CALL Merchant name/logo lookup, geo-enrichment, market data Per call
NOTIFICATION_SEND Push notification, SMS, email via Pinpoint Per send
DOCUMENT_STORE S3 put/get for KYC documents, receipts Per MB
CDC_EVENT Kinesis Firehose record delivered to Snowflake Per 1,000 records
DECISION_PUBLICATION Snowflake write-back to Postgres (MOD-079 pattern) Per write

Event schema

Every usage event emitted to the bank-platform EventBridge bus has the following structure:

{
  "source": "bank.platform.metering",
  "detail-type": "usage_event",
  "detail": {
    "schema_version": "1.0",
    "idempotency_key": "<uuid>",
    "tenant_id": "<licensee or 'self' for own platform>",
    "module_id": "MOD-XXX",
    "facility_id": "SD0X",
    "event_type": "API_CALL",
    "quantity": 1,
    "resource_units": 0.001,
    "resource_unit_type": "LAMBDA_GB_SECONDS",
    "environment": "prod",
    "timestamp": "2026-04-15T10:23:00Z",
    "correlation_id": "<originating request id>"
  }
}

tenant_id is "self" for the bank's own customers (not SaaS licensees). This allows the same pipeline to generate both external SaaS billing and internal unit economics.

Pipeline

Lambda invocation (any module)
  └─ Lambda layer intercept → emit usage_event to EventBridge
        └─ EventBridge rule → Kinesis Data Firehose
              └─ S3 landing zone (compressed JSON, partitioned by date/tenant)
                    └─ Snowflake Snowpipe → metering.usage_events
                          └─ (available within ~5 minutes of event)

External API costs (Marketplace subscriptions, NZFMA, enrichment providers) are collected separately via a daily Lambda that reads invoices or usage reports from each provider and writes to metering.external_api_costs.

Tagging governance

Every AWS resource deployed for a tenant (Lambda, S3, Kinesis, DynamoDB, SQS, API Gateway stage) must carry the tags: - tenant_id — the licensee identifier (or self) - module_id — the module owning the resource - environment — prod / staging / dev

Missing tags cause the resource's costs to fall to the unattributed overhead bucket. IaC (SST/CDK) enforces required tags at the synthesis step. CI runs aws-cdk-lib/aws-config tag compliance rules before deploying to prod.

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy LOG All billable usage events are logged with tenant, module, resource type, and timestamp — provides the immutable audit trail for SaaS billing and internal cost accounting.

MOD-099 — Infrastructure cost reports

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Billing schema ownership

MOD-099 owns the billing.* Snowflake schema. This is an explicit ownership transfer — MOD-098 provisioned BILLING.TENANT_MODULES and BILLING.TENANT_TIERS as stubs to unblock its own dbt models, pending MOD-099 shipping.

When MOD-099 is built, the agent must:

  1. Provision BILLING.TENANT_MODULES and BILLING.TENANT_TIERS in MOD-099-usage-billing-report/infra/snowflake/ with the canonical schema (see MOD-098 design doc for the current stub definitions — MOD-099 should adopt these column shapes and extend them as needed).
  2. Add BILLING.INVOICES and BILLING.INVOICE_LINE_ITEMS tables to the same infra/snowflake/ directory.
  3. Remove billing.sql and billing_*.sql from MOD-098-cost-attribution-engine/infra/snowflake/ in a co-ordinated deployment — MOD-099 must be deployed first so the tables exist before MOD-098's DDL files are removed. Use CREATE TABLE IF NOT EXISTS in MOD-099's scripts so a re-run against an already-populated schema is safe.
  4. Update dbt/models/MOD-098-cost-attribution-engine/sources.yml to reference the billing.* tables as an external source owned by MOD-099 rather than as a source owned by MOD-098 itself.

Deployment order constraint: MOD-099 DDL must be applied before MOD-098's stub DDL is removed. The CI/CD workflow for the MOD-098 cleanup commit must depend on a successful MOD-099 deploy.


Purpose

MOD-099 surfaces the cost attribution data computed by MOD-098 as a transparent, usable report for two audiences: the licensee (who needs to understand what they are paying for and why) and internal finance (who need to understand unit economics, gross margin, and platform cost trends).

The transparency principle is a deliberate commercial decision. Licensees who can see their usage breakdown in real time are less likely to dispute invoices, more likely to trust the platform, and better positioned to forecast their own costs.

Licensee-facing dashboard

Available in the back-office portal under Billing & Usage. Accessible to any user with the BILLING_VIEWER or BILLING_ADMIN access grant on their tenant context.

Current period summary card

Billing period: 1 Apr – 30 Apr 2026
Customer levy:       $4,200.00   (1,400 active customers × $3.00)
Facility fees:       $2,750.00   (11 modules × $250.00/month)
Variable usage:      $  830.40   (see breakdown)
Infrastructure:      $  412.00   (passthrough — see detail)
─────────────────────────────────
Estimated total:     $8,192.40
(Final invoice issued 3 May 2026)

Variable usage drill-down

Resource type Included Used Overage Rate Charge
Snowflake credits 500 823 323 $0.50/credit $161.50
ML inferences 50,000 94,200 44,200 $0.003/call $132.60
Enrichment API calls 100,000 187,400 87,400 $0.002/call $174.80
Notification sends 20,000 51,300 31,300 $0.008/send $250.40
Document storage 10 GB 13.9 GB 3.9 GB $28.80/GB/mo $111.10

Module-level breakdown

Module Facility fee ML inferences Enrichment calls Snowflake credits Total
MOD-009 eIDV $250 12,400 calls 18 credits $274.80
MOD-039 Risk scoring $250 94,200 inferences 380 credits $564.90
MOD-041 Categorisation $250 175,000 calls 425 credits $963.50

Infrastructure passthrough (optional toggle)

AWS service Cost
Lambda $84.20
API Gateway $63.40
Kinesis Firehose $41.10
S3 $28.90
DynamoDB $19.80
EventBridge + SQS $12.30
Secrets Manager $8.20
Snowflake (dedicated warehouse) $154.10
Total passthrough $412.00

Trend chart

30-day rolling chart showing daily estimated cost, with annotations for significant events ("New module activated", "Customer count crossed 1,000").

Export

  • Download current period breakdown as CSV or PDF
  • API endpoint: GET /billing/usage?period=2026-04&format=json (authenticated, tenant-scoped)

Internal finance view

Accessible to Finance and Platform teams in the back-office portal.

Gross margin by tenant

Tenant Revenue AWS cost Snowflake cost External APIs Total cost Gross margin
Tenant A $8,192 $264 $154 $112 $530 93.5%
Tenant B $3,450 $118 $67 $44 $229 93.4%

Unit economics

Module Cost per customer per month At 1,000 customers At 10,000 customers
MOD-009 eIDV $0.12 $120 $890 (scale discount)
MOD-039 Risk score $0.38 $380 $2,800
MOD-041 Categorisation $0.69 $690 $4,100

Unattributed cost monitor

Any AWS costs with no tenant_id tag appear here. A non-zero unattributed bucket is a tagging governance gap and triggers an engineering alert.


Invoice generation

At the end of each billing period, MOD-099 generates: - One billing.invoices record per tenant with the period total - One billing.invoice_line_items record per billing component (customer levy, each facility fee, each variable line, passthrough) - Invoice status flows: DRAFTISSUEDPAID / DISPUTED - PDF invoice generated and stored in S3 (ADR-028); URL written to the invoice record - bank-platform.invoice_issued EventBridge event triggers notification to the licensee's billing contact

Policies satisfied:

Policy Mode Description
REP-001 — Regulatory Reporting Policy LOG Infrastructure cost history is retained as queryable Snowflake records — provides audit trail for AWS and Snowflake spend by service, period, and attributed tenant.

MOD-100 — External asset connector

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

Connects to external financial data providers — primarily Akahu (NZ) and direct AU superannuation fund APIs — to retrieve a customer's external asset data under explicit OAuth 2.0 consent. Normalises provider-specific response shapes into the canonical assets and asset_party_relationships schema in SD01 Postgres, making external assets visible to the wealth intelligence engine and the app net worth dashboard.

External assets currently in scope: - KiwiSaver accounts (NZ) — balance, fund type (conservative/balanced/growth/aggressive), provider name, last contribution date - AU superannuation (Phase 2) — balance, fund name, member number, last contribution date - Held-away bank accounts (optional) — balances at other NZ banks via Akahu (useful for bank migration UX)

Architecture

Akahu connection is a scheduled Lambda (daily, off-peak) that: 1. Fetches the list of consenting customers with valid Akahu tokens from operating_contexts.akahu_consent 2. For each customer, calls the Akahu /accounts and /balances endpoints 3. Maps the response to the assets schema (asset_type = 'KIWISAVER' or 'SUPERANNUATION' or 'EXTERNAL_DEPOSIT') 4. Writes normalised records into a staging table in SD07 Snowflake (FR-402); a write-back Lambda then upserts from the staging table into SD01 Postgres assets + asset_party_relationships 5. Fires bank-platform.external_asset_updated on EventBridge with customer_id, asset_id, asset_type, provider_name, balance_nzd (or balance_aud), as_at — emitted after the Postgres upsert succeeds

The normalisation layer is provider-agnostic: adding a new Akahu-connected provider requires only a mapping configuration entry, not code changes.

Customer consent is initiated from the app (MOD-075 external account linking flow). The Akahu OAuth flow redirects through the provider's consent screen. On completion, the Akahu consent token is stored in operating_contexts.akahu_consent with scope, expiry, and audit timestamp.

Consent is revocable at any time from the app. Revocation triggers immediate halt of retrieval and deletion of cached asset records within 24 hours (Privacy Act 2020 s 22 — retention only as long as purpose requires).

Data staleness

KiwiSaver unit prices are published each business day by fund managers. Retrieval runs daily at 02:00 NZST. The assets.last_refreshed_at timestamp is surfaced in the app so customers see when the balance was last updated. The UX explicitly labels these as "as at [date]" — not live.

If retrieval fails for a customer three consecutive days, a bank-app.external_asset_retrieval_failed alert is fired and a push notification prompts the customer to re-authorise.

Compliance notes

This module retrieves data under customer consent — not under the bank's own authority. The bank acts as a data recipient, not a data holder, for external assets. Privacy Act 2020 s 22 (use limitation) applies: data is used only for the purpose consented to (financial position display and wealth insights).

The module does not make any investment recommendations. It surfaces factual data (balance, fund type, contribution history) only.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Akahu consent token, scope, and expiry recorded per customer — consent audit trail maintained for Privacy Act 2020 compliance.
PRI-003 — Personal Information Retention & Destruction Policy GATE External asset retrieval halts immediately on consent revocation and cached records are deleted within 24 hours — no data retained beyond consent scope.

MOD-102 — Snowflake account configuration & governance

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

Provisions and governs the Snowflake account configuration. This is an IaC module — it does not contain Lambda application code. It runs via the bank-platform CI/CD pipeline when configuration changes are merged, and must be fully deployed before any module that reads from or writes to Snowflake can operate.

This module is the implementation of the decisions in ADR-002 and ADR-035.

Execution pattern

All Snowflake configuration is expressed as versioned SQL scripts — no Terraform, no provider state file. Snowflake's native IaC is SQL; every object (warehouse, database, role, integration, masking policy, tag, task) is created and managed via CREATE OR REPLACE / ALTER DDL. Scripts are version-controlled in bank-platform under snowflake/setup/ and applied in order by the CI/CD pipeline.

dbt core handles all data transformation layer DDL (views, dynamic tables, incremental models). The SQL setup scripts cover account-level objects only — warehouses, databases, schemas, roles, integrations, network policy, and tag/masking infrastructure.

Account structure

Single Snowflake account for all environments. Environments are namespaced by convention, not by separate accounts. This preserves the ability to use Snowflake's Dynamic Data Masking to control what lower-environment users see when accessing replicated production data.

Database namespace convention

BANK_{ENV}_{DOMAIN}
Environment Core KYC AML Payments Credit Risk Platform
prod BANK_PROD_CORE BANK_PROD_KYC BANK_PROD_AML BANK_PROD_PAYMENTS BANK_PROD_CREDIT BANK_PROD_RISK BANK_PROD_PLATFORM
uat BANK_UAT_CORE BANK_UAT_KYC BANK_UAT_AML
dev BANK_DEV_CORE BANK_DEV_KYC BANK_DEV_AML

Schemas within each database mirror the Neon schema names (e.g. BANK_PROD_CORE contains schemas ACCOUNTS, POSTINGS, TREASURY, CONTEXTS, ASSETS).

Lower environment data strategy

Two options, not mutually exclusive:

  • Synthetic data — dev and UAT databases populated from synthetic data generators; no prod data involved
  • Masked prod data — dev and UAT databases populated via Snowflake replication with Dynamic Data Masking policies active; PII columns are masked at query time for all non-privileged roles

Both options are available. The masking infrastructure is provisioned by this module regardless of which strategy is in use at any given time.

Warehouses

Production carries dedicated per-workload warehouses. Dev and UAT share a single minimal warehouse to minimise credit consumption.

Production warehouses

Warehouse Size Use Auto-suspend
PROD_ETL_WH Small CDC ingestion, Firehose landing, batch loads 60s
PROD_ANALYTICS_WH Small (scales to Medium) Interactive queries, BI, ad-hoc 60s
PROD_RISK_WH Medium Risk model computation, LCR/NSFR, stress tests 60s
PROD_DECISIONS_WH X-Small Decision result publication (MOD-079) 30s
PROD_DBT_WH Small dbt core model runs (transformation pipeline) 60s

Resource monitors cap daily credit consumption per warehouse with alerts at 75% and a hard stop at 100%.

Non-production warehouses

Warehouse Size Use Environments
NONPROD_WH X-Small All workloads (ETL, analytics, dbt runs) dev, uat

A single warehouse per non-prod environment prevents idle credit burn from multiple suspended warehouses.

RBAC architecture

Role hierarchy

ACCOUNTADMIN  (Snowflake built-in — break-glass only, credentials in PAM)
  └─ SYSADMIN
        └─ BANK_FUNCTIONAL_ROLE
              ├─ BANK_PROD_CORE_ROLE        (PROD — bank_prod_core DB, read/write)
              ├─ BANK_PROD_KYC_ROLE         (PROD — bank_prod_kyc DB)
              ├─ BANK_PROD_AML_ROLE
              ├─ BANK_PROD_PAYMENTS_ROLE
              ├─ BANK_PROD_CREDIT_ROLE
              ├─ BANK_PROD_RISK_ROLE        (PROD — read/write)
              ├─ BANK_PROD_PLATFORM_ROLE    (PROD — ETL write)
              ├─ BANK_NONPROD_CORE_ROLE     (dev + uat — all BANK_DEV_CORE + BANK_UAT_CORE)
              ├─ BANK_NONPROD_KYC_ROLE
              ├─ … (one per domain for non-prod)
              ├─ BANK_ANALYTICS_ROLE        (SELECT on approved prod views — masked)
              ├─ BANK_REPORTING_ROLE        (SELECT on regulatory reporting schemas — prod)
              ├─ BANK_DEVELOPER_ROLE        (SELECT on all dev + uat databases; no prod access)
              └─ BANK_DBT_ROLE              (CREATE/REPLACE on transformation schemas — dbt service account)

SECURITYADMIN
  └─ BANK_SECURITY_ROLE   (manages grants and masking policy assignments — not used for data access)

Developer access

BANK_DEVELOPER_ROLE grants: - SELECT on all BANK_DEV_* and BANK_UAT_* databases - No access to BANK_PROD_* databases - Usage on NONPROD_WH

Developer role assignment is managed via EntraID AD group sync (see Identity section). No manual Snowflake user creation or role grants for individual developers.

Service account roles

Each system domain's Lambda execution role authenticates to Snowflake via key-pair authentication and assumes the corresponding prod or non-prod domain role. Keys are stored in Secrets Manager (MOD-045), scoped per environment.

Identity and access — EntraID SSO

EntraID is external to the bank software boundary. This section documents the Snowflake-side configuration required and what must be configured in EntraID by the identity team.

Snowflake-side setup (provisioned by this module)

-- Enable SCIM for EntraID provisioning
CREATE OR REPLACE SECURITY INTEGRATION bank_entra_scim
  TYPE = SCIM
  SCIM_CLIENT = 'AZURE'
  RUN_AS_ROLE = BANK_SECURITY_ROLE;

-- SAML SSO integration for interactive login
CREATE OR REPLACE SECURITY INTEGRATION bank_entra_sso
  TYPE = SAML2
  SAML2_ISSUER = 'https://sts.windows.net/{tenant-id}/'
  SAML2_SSO_URL = 'https://login.microsoftonline.com/{tenant-id}/saml2'
  SAML2_PROVIDER = 'CUSTOM'
  SAML2_X509_CERT = '<certificate from EntraID app registration>';

EntraID configuration required (identity team — external)

Item Detail
Enterprise app Create "Snowflake" enterprise app in EntraID tenant
SCIM provisioning Enable SCIM provisioning to Snowflake SCIM endpoint; provision bearer token from the SCIM integration above
User lifecycle SCIM creates Snowflake users on EntraID assignment; disables/deletes on offboarding
AD group → role mapping bank-snowflake-developersBANK_DEVELOPER_ROLE; bank-snowflake-analystsBANK_ANALYTICS_ROLE; bank-snowflake-reportingBANK_REPORTING_ROLE; domain service accounts use key-pair auth, not SSO
MFA policy Conditional Access policy requiring MFA for all Snowflake SSO sessions

Service account connections (Lambda, dbt, CI/CD) use key-pair authentication and are not subject to SSO. Only human interactive logins go via EntraID SAML.

Integrations

Storage integration — S3

S3 access for CDC landing, Iceberg data lake, and file export stages:

CREATE OR REPLACE STORAGE INTEGRATION bank_s3_integration
  TYPE = EXTERNAL_STAGE
  STORAGE_PROVIDER = 'S3'
  ENABLED = TRUE
  STORAGE_ALLOWED_LOCATIONS = (
    's3://bank-snowflake-prod-landing/',
    's3://bank-iceberg-prod/',
    's3://bank-export-prod/'
  );

Notification integration — SQS

SQS for Snowpipe auto-ingest from CDC Firehose landing:

CREATE OR REPLACE NOTIFICATION INTEGRATION bank_sqs_integration
  TYPE = QUEUE
  NOTIFICATION_PROVIDER = AWS_SQS
  ENABLED = TRUE
  AWS_SQS_ARN = '<SQS ARN from MOD-104>';

Git integration — CI/CD and dbt

Snowflake Git integration connects directly to the bank-platform repository. This enables dbt models to be executed natively within Snowflake without an external orchestrator, using Snowflake's embedded dbt support (Snowflake Notebook + dbt Core via Snowpark Container Services):

CREATE OR REPLACE API INTEGRATION bank_github_integration
  API_PROVIDER = git_https_api
  API_ALLOWED_PREFIXES = ('https://github.com/totara-bank/')
  ENABLED = TRUE;

CREATE OR REPLACE GIT REPOSITORY bank_platform_repo
  API_INTEGRATION = bank_github_integration
  ORIGIN = 'https://github.com/totara-bank/bank-platform';

dbt models are fetched from bank_platform_repo and executed by Snowflake Tasks on the PROD_DBT_WH. This replaces any external dbt runner — transformation runs are entirely internal to Snowflake.

Network policy

PrivateLink is not configured at this stage (cost not justified for current scale). Network access is controlled by an IP allowlist network policy:

CREATE OR REPLACE NETWORK POLICY bank_network_policy
  ALLOWED_IP_LIST = (
    '<AWS NAT gateway EIP — NZ>',
    '<AWS NAT gateway EIP — AU>',
    '<CI/CD runner IP range>',
    '<Admin bastion IP — PAM-gated, MOD-046>'
  );

ALTER ACCOUNT SET NETWORK_POLICY = bank_network_policy;

No public internet access from application code. All Lambda-to-Snowflake traffic exits via the AWS NAT gateway with a fixed EIP, which is included in the allowlist. PrivateLink is documented as the natural upgrade path when data volume and cost profile warrant it.

dbt core — transformation pipeline

dbt core handles all data transformation: model definitions, incremental materialisation, data mart construction, Dynamic Table definitions, and regulatory view construction. Schemachange is not used.

dbt runs natively within Snowflake via the Git repository integration and Snowflake Tasks. The embedded dbt execution path means no external orchestration dependency for transformation runs.

Responsibilities: - Schema creation (CREATE SCHEMA IF NOT EXISTS) for transformation layers - Model materialisation (views, tables, incremental, Dynamic Tables) - Data lineage documentation (dbt docs) - Data tests (schema tests, custom tests on business rules)

Each system domain that writes to Snowflake has a corresponding dbt project directory under bank-platform/dbt/. The BANK_DBT_ROLE / BANK_DBT_WH are the execution context.

PII tagging and dynamic data masking

Object tags

A PII_CLASSIFICATION tag is applied to all columns containing personal or sensitive data:

CREATE OR REPLACE TAG pii_classification
  ALLOWED_VALUES 'PII_HIGH', 'PII_MEDIUM', 'FINANCIAL', 'INTERNAL', 'PUBLIC';

PII_HIGH covers: name, date of birth, tax identifiers (IRD/TFN), passport/driver licence numbers, biometric data. PII_MEDIUM covers: email, phone, address, account numbers. FINANCIAL covers: balances, transaction amounts, credit scores.

Tags are applied to columns as part of the dbt model definitions (meta: {pii_classification: PII_HIGH}) and enforced via the BANK_SECURITY_ROLE governance process.

Masking policies

Dynamic Data Masking policies are bound to the PII_CLASSIFICATION tag. In BANK_PROD_* databases: - Roles with BANK_PROD_{DOMAIN}_ROLE see unmasked data (service accounts only) - BANK_ANALYTICS_ROLE and BANK_REPORTING_ROLE see masked values (e.g. '***' for names, last-4-digits for account numbers) - BANK_DEVELOPER_ROLE has no access to prod databases at all

In BANK_DEV_* and BANK_UAT_* databases, masking policies are present but pass-through — lower environments hold either synthetic data or masked-at-source prod data. The same policy infrastructure applies in all environments for consistency.

Internal orchestration

Snowflake Tasks and Streams drive all internal pipeline triggers. No external orchestrator is required for Snowflake-internal workloads.

Key tasks provisioned by this module: - CDC ingestion trigger — Task on PROD_ETL_WH polling the Snowpipe ingest completion stream - dbt model refresh — Task on PROD_DBT_WH executing the dbt Git-based run on schedule - Dynamic Table refresh scheduling — managed by Snowflake natively once DTs are defined by dbt - Regulatory report generation — Task on PROD_RISK_WH triggering MOD-086 report queries on schedule

Task dependency graphs are defined in the SQL setup scripts. All tasks are BANK_FUNCTIONAL_ROLE-owned.

File export

COPY INTO external S3 stages is the mechanism for all outbound file generation from Snowflake. Use cases requiring file export:

Use case Stage Format Consumer
Regulatory submissions bank-export-prod/regulatory/ CSV / pipe-delimited RBNZ, FMA, AUSTRAC — via MOD-086
Audit data extracts bank-export-prod/audit/ Parquet External auditors, internal audit
Third-party data feeds bank-export-prod/partners/ CSV Configured per agreement
Credit bureau submissions bank-export-prod/credit-bureau/ Fixed-width / CSV MOD-059

The bank_s3_integration storage integration covers all export stages. Export tasks are triggered by Snowflake Tasks (see internal orchestration above).

Observability → MOD-076

Snowflake account metadata is exported to the observability platform (MOD-076) for monitoring, alerting, and cost governance. A scheduled Task runs on PROD_ANALYTICS_WH to export from ACCOUNT_USAGE views to a nominated S3 path, where MOD-076 ingests it.

Exported datasets:

Source view Content Frequency
ACCOUNT_USAGE.QUERY_HISTORY Query execution, latency, credit consumption per query Hourly
ACCOUNT_USAGE.METERING_HISTORY Credit consumption per warehouse Daily
ACCOUNT_USAGE.TASK_HISTORY Task execution status, errors, duration Hourly
ACCOUNT_USAGE.COPY_HISTORY Snowpipe and COPY INTO completions/failures Hourly
ACCOUNT_USAGE.LOGIN_HISTORY Authentication events (success/failure) Daily
ACCOUNT_USAGE.GRANT_PRIVILEGES_HISTORY Role grant changes Daily

Alerting on: warehouse credit overage (resource monitor threshold), task failures, login anomalies. Alerts route through MOD-076's alerting pipeline.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE Snowflake RBAC roles enforce schema-level access boundaries — no system domain can read another domain's raw tables without an explicit cross-domain read grant reviewed by the data governance board.
DT-002 — Cybersecurity Policy AUTO All schema transformations are applied through the version-controlled dbt core pipeline — no ad-hoc schema modifications permitted in production.
DT-004 — Data Governance Policy AUTO The single governed CDC pipeline (MOD-042) is the only path for operational data to enter Snowflake — enforced by storage integration policy and warehouse grants.
GOV-007 — Conflicts of Interest Policy LOG All warehouse activity, login events, and grant changes are captured in Snowflake's account_usage schema and retained for seven years via the ACCOUNT_USAGE database.

MOD-103 — Neon database platform bootstrap

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

Provisions all Neon Postgres infrastructure: the project, environment branches, databases, roles, connection pools, and schema migration pipeline. This is an IaC module — it does not contain Lambda application code. It runs via the bank-platform CI/CD pipeline on configuration change and must be fully deployed before any module that reads from or writes to Postgres can operate.

This module is the implementation of ADR-024.

Execution pattern

Unlike runtime modules, this module uses: - Neon API scripts — plain CI steps that call the Neon REST API directly to create/update project configuration, branches, databases, and roles. No Terraform state file or provider. - Flyway for schema migrations, integrated into each system domain's deploy pipeline - Deployed via CI/CD on merge to main; not a running process

Using direct API scripts keeps the provisioning footprint small and avoids a second state management layer. The Neon project and branch model is simple enough that idempotent API calls (create-if-not-exists) are sufficient.

Project and branch structure

One Neon project for the entire platform. Environments are persistent branches; preview environments are ephemeral branches off dev.

bank  (single Neon project)
  ├─ prod    (persistent branch — production databases)
  ├─ uat     (persistent branch — UAT / release verification)
  ├─ dev     (persistent branch — CI baseline; shared team instance)
  │    └─ pr-{number}  (ephemeral preview branches, auto-created per PR, auto-deleted on close)
  └─ (local development points at dev branch or a personal preview branch)

Each branch is an instant zero-copy clone of its parent — preview branches cost near-zero storage overhead. Branch-level isolation ensures dev and UAT schemas and data cannot cross-contaminate prod.

Databases (per branch)

Each branch carries the same set of databases, one per system domain:

Database System domain Primary schemas
bank_core SD01 Core Banking accounts, postings, treasury, contexts, assets
bank_kyc SD02 KYC Platform kyc, party, banking, regulatory
bank_aml SD03 AML Monitoring aml
bank_payments SD04 Payments payments
bank_credit SD05 Credit credit
bank_app SD08 App app, access

SD06 (Risk Platform) and SD07 (Data Platform) write to Snowflake, not Neon — no Neon databases for those domains.

Role provisioning (per database, per branch)

Three roles are provisioned per database. Role names are the same across all branches — environment isolation is provided by the branch, not the role name.

Role Permissions Used by
{domain}_app_user SELECT, INSERT, UPDATE, DELETE on all tables in domain schema Lambda execution role
{domain}_migrate_user DDL — CREATE, ALTER, DROP on domain schema Migration pipeline only
{domain}_readonly SELECT on all tables in domain schema CDC replication source (MOD-042), reporting

Passwords are generated at provisioning time and stored in Secrets Manager (MOD-045), scoped per branch (dev, uat, prod secrets are separate paths). App roles connect via PgBouncer. Migration role uses a direct connection (bypasses pooler — DDL requires session mode).

No cross-domain database connections from application code. The _readonly role is the only permitted cross-domain access path, used exclusively by MOD-042 for CDC replication from prod.

Connection pooler configuration

Neon's built-in PgBouncer is configured in transaction pooling mode — required for Lambda (ephemeral, high-concurrency) workloads. Exact pool sizes and max connection limits are defined as IaC variable overrides per environment. The principle:

  • prod — sized for production concurrency; generous limits with head-room for peak
  • uat — moderate capacity; sufficient for full acceptance test suites running concurrently
  • dev — minimal; enough for CI pipelines, not sized for sustained load
  • preview branches — minimal; single-developer or single-pipeline usage only

Connection strings for each database and branch are stored in Secrets Manager as structured secrets referenced by Lambda environment variables.

Direct (non-pooled) connection strings are maintained for: - Migration operations (DDL requires session mode) - CDC logical replication source (requires persistent connection — MOD-042) - PAM-gated DBA access (MOD-046)

Schema migration pipeline (Flyway)

Each system domain repository contains a db/migrations/ directory with versioned SQL files (V001__description.sql, V002__description.sql). The migration pipeline:

  1. Connects to the domain database on the target branch using the migrate_user direct connection
  2. Checks the flyway_schema_history table for applied versions
  3. Applies pending migrations in version order within a transaction
  4. Fails fast — any error halts the deploy pipeline before Lambda code is deployed
  5. Every forward migration (Vxxx__description.sql) must have a corresponding undo migration (Uxxx__description_rollback.sql)

Preview branches: CI creates a Neon branch from dev for each PR via the Neon API. Flyway runs the PR's migrations against the branch. The branch is deleted when the PR closes (merged or abandoned).

Promotion path: Migrations are applied independently per branch as the environment advances through the promotion pipeline. The same migration files run on dev → uat → prod in sequence; they are never applied directly to prod without first passing dev and uat.

Data in lower environments

Neon branching means that when a preview or dev branch is created from dev, it inherits whatever data is in dev. The dev branch holds synthetic data only — prod data never flows to dev or UAT. Synthetic data generation is the responsibility of each system domain's test fixtures.

Data residency

The Neon project is provisioned in AWS region ap-southeast-2 (Sydney) for both NZ and AU environments — Neon's nearest available region. Customer data does not leave this region. This satisfies the NZ Privacy Act 2020 and AU Privacy Act 1988 requirements for data localisation when processing is in a country with comparable privacy laws.

If NZ-only data residency is required by future regulation, the evolution path is Neon's planned NZ region or migration to Neon on RDS in ap-southeast-4 (Melbourne), per ADR-024.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE Each system domain is provisioned with its own Postgres database and role — cross-domain direct DB connections are structurally prevented by the role provisioning model.
PRI-001 — Privacy Policy GATE Database roles and column-level permissions are configured at bootstrap to enforce the data minimisation principle — application roles are granted access only to the schemas and columns they require.
PRI-003 — Personal Information Retention & Destruction Policy AUTO Neon project is provisioned in the correct AWS region for data residency (NZ and AU environments in region-appropriate endpoints), satisfying the data localisation obligation.

MOD-104 — AWS shared infrastructure bootstrap

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

Provisions all shared AWS infrastructure that every system domain depends on. This is an IaC module — it contains CDK/SST stacks, not Lambda application code. It runs via the bank-platform CI/CD pipeline and must be fully deployed before any other module in any system domain can be deployed.

This is the bottom of the dependency tree. No other module can be deployed without it.

Execution pattern

Unlike runtime modules, this module uses: - SST v3 Ion (home: "aws") for all AWS resource provisioning — consistent with ADR-025, which establishes Pulumi (via SST Ion) as the IaC layer for all repos - SST Ion self-bootstraps its state infrastructure on first deploy: it creates its own S3 state bucket and records the bucket name in SSM Parameter Store at /sst/bootstrap. No manual state bucket creation is required — npx sst deploy with valid AWS credentials is sufficient. - Deployed once at platform bootstrap, then updated on config change via CI/CD - Runs before all other repos' deployment pipelines - Lives in a bootstrap/ directory in the bank-platform repo, separate from runtime Lambda code

EventBridge buses

One custom event bus per system domain. Each bus has: - A cross-account resource policy permitting events from the corresponding Lambda execution role - An archive rule retaining all events for 30 days (replay capability) - A dead-letter queue (SQS) for undeliverable events

Bus name Owner Purpose
bank-core SD01 Account lifecycle, posting, balance events
bank-kyc SD02 KYC status, onboarding, CDD events
bank-aml SD03 Alert created, case status, STR filed events
bank-payments SD04 Payment initiated, settled, failed events
bank-credit SD05 Application, decisioning, drawdown events
bank-risk-platform SD06 Score updates, rate publications, capital alerts
bank-platform SD07 CDC, usage events, external asset updates
bank-app SD08 Device registered, session, notification events

Cross-bus subscriptions (where one domain needs events from another) use EventBridge rules with a cross-bus target. The event catalogue documents all rules and named consumers.

S3 buckets

Bucket Purpose Encryption Lifecycle
bank-iceberg-prod Iceberg data lake — CDC output (MOD-042) KMS (financial data key) Glacier after 90d, delete after 7y
bank-firehose-landing Kinesis Firehose landing zone KMS (operational key) Auto-expire 24h
bank-documents-prod Customer document store (MOD-028) KMS (PII key) Glacier after 1y, delete after 7y
bank-artefacts Deployment artefacts (Lambda ZIPs, CDK assets) SSE-S3 Delete after 90d
bank-reports-prod Regulatory report output, invoice PDFs KMS (financial data key) Glacier after 1y, delete after 7y

All buckets: SSL-only access enforced, public access blocked, versioning enabled.

KMS keys

One customer-managed key (CMK) per data classification level:

Key alias Classification Used by
bank/pii Customer PII — names, DOB, addresses, tax IDs Document store, KYC databases, Cognito
bank/financial Financial records — transactions, balances, rates Iceberg, report output, Snowflake integration
bank/operational Operational data — logs, configs, artefacts Firehose, Secrets Manager, CloudTrail

Key policies: usage rights granted only to the Lambda execution roles of modules that handle that data classification. Key rotation is automatic (annual). Key ARNs are exported as SSM Parameter Store parameters (/bank/{env}/kms/{alias}/arn) and referenced by all downstream IaC modules.

Kinesis Data Firehose streams

Stream Source Destination Purpose
bank-cdc-stream Neon logical replication (MOD-042) S3 bank-iceberg-landing/ CDC pipeline landing
bank-usage-events EventBridge bank-platform bus S3 + Snowflake Usage metering (MOD-097)

Both streams: 128 MB / 300s buffer, KMS encryption, CloudWatch error metrics, SQS DLQ.

Cognito user pools

Two user pools per environment, per ADR-026, ADR-027, and ADR-042:

Pool Users MFA Custom attributes
bank-customers-{env} All retail customers — NZ and AU Required (biometric app + TOTP fallback) custom:user_id, custom:party_id, custom:jurisdiction
bank-staff-{env} Internal employees + contractors Required (TOTP) custom:staff_id

NZ and AU customers share a single pool. Jurisdiction is expressed as a custom attribute (custom:jurisdiction = NZ \| AU) set at onboarding and emitted as a JWT claim on every token. See jurisdiction runtime model for how this flows through the system.

App clients provisioned for: mobile app (public client, PKCE), web app (public client, PKCE), back-office web (confidential client), internal API Gateway authoriser. Token configuration: ID token 1h, access token 1h, refresh token 30d.

IAM Lambda execution roles

One IAM role per system domain, following least-privilege:

Role Trust Key permissions
BankCoreRole Lambda (bank-core) Neon secrets read, EventBridge put (bank-core bus), KMS decrypt (financial)
BankKycRole Lambda (bank-kyc) Neon secrets read, EventBridge put (bank-kyc bus), S3 put (documents), KMS decrypt (PII + financial)
BankAmlRole Lambda (bank-aml) Neon secrets read, EventBridge put (bank-aml bus), KMS decrypt (financial)
BankPaymentsRole Lambda (bank-payments) Neon secrets read, EventBridge put (bank-payments bus), KMS decrypt (financial)
BankCreditRole Lambda (bank-credit) Neon secrets read, EventBridge put (bank-credit bus), KMS decrypt (financial + PII)
BankRiskRole Lambda (bank-risk-platform) Snowflake key read, EventBridge put (bank-risk-platform bus), KMS decrypt (financial)
BankPlatformRole Lambda (bank-platform) All event buses read, S3 read/write (iceberg + firehose), Kinesis put, Snowflake key read
BankAppRole Lambda (bank-app) Neon secrets read, EventBridge put (bank-app bus), Cognito admin, KMS decrypt (PII)

All roles: mandatory tagging enforcement via IAM condition (aws:RequestedRegion, aws:ResourceTag/module_id).

CloudTrail and SSM

  • CloudTrail: Enabled across all accounts from day one. All management and data events logged to bank-artefacts/cloudtrail/. Retained 90 days hot, 7 years cold.
  • SSM Parameter Store: Used for non-secret configuration shared across repos (KMS key ARNs, EventBridge bus ARNs, S3 bucket names, Cognito pool IDs). Naming convention: /bank/{env}/{service}/{parameter}.

Deployment note

This module is deployed via a dedicated infra stage in the CI/CD pipeline that runs before any system domain pipeline. On first deploy (bootstrap), a human operator must run aws sso login and assume the bootstrap role — subsequent updates are fully automated.

The bank-platform repo contains a bootstrap/ directory with the CDK stacks for this module, separate from the runtime Lambda code.

Policies satisfied:

Policy Mode Description
GOV-005 — Financial Accountability Regime (FAR) Policy GATE All AWS resources are tagged with tenant_id, module_id, and environment at IaC synthesis time — untagged resources are blocked from deployment by SCP and the tagging compliance gate in MOD-097.
GOV-006 — Internal Audit Policy LOG CloudTrail is enabled across all accounts from bootstrap — every AWS API call is logged from day one, satisfying the operational audit trail requirement.
DT-002 — Cybersecurity Policy GATE KMS CMKs are provisioned per data classification level; encryption at rest is enforced by S3 bucket policy and Kinesis encryption settings — unencrypted data storage is not permitted.

MOD-156 — CI/CD pipeline platform

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-156 is the CI/CD platform that governs how every module in every code repository is built, tested, and deployed. It provides two reusable GitHub Actions workflow templates, a wiki-driven workflow generator, cross-repo build ordering via repository_dispatch, and the GitHub configuration (Environments, branch protection, OIDC federation) that enforces the bank's change and deployment standards across all eight repositories.

Without this module, each code repository would need its own bespoke pipeline configuration, build-sequence dependencies would be undocumented, and the audit trail required by DT-007 and OPS-006 would be incomplete.

Architecture

bank-wiki (source of truth)
  └── source/entities/modules/*.yaml      ← dependency graph, build sequence
  └── scripts/generate-workflows.py       ← emits per-repo workflow YAML files
  bank-platform/
    .github/
      workflows/reusable-lambda.yml       ← template for Lambda modules
      workflows/reusable-iac.yml          ← template for IaC modules
        ├── called by ───────────────────────► bank-core/.github/workflows/
        │                                      bank-kyc/.github/workflows/
        │                                      bank-aml/.github/workflows/
        │                                      bank-payments/.github/workflows/
        │                                      bank-credit/.github/workflows/
        │                                      bank-risk-platform/.github/workflows/
        │                                      bank-app/.github/workflows/
        │                                      bank-platform/.github/workflows/
        └── update-wiki.py (on Built)
              └── repository_dispatch ──────► downstream dependent repos

Reusable templates

Two GitHub Actions reusable workflows live in bank-platform/.github/workflows/:

reusable-lambda.yml — for Lambda-type modules. Parameters: module_id, module_dir, node_version, stage. Steps: npm install, TypeScript typecheck, unit tests (≥80% coverage gate), integration tests (RUN_INTEGRATION=1), SST deploy via OIDC role, update-wiki.py status push.

reusable-iac.yml — for IaC-only modules. Parameters: module_id, module_dir, stage. Steps: npm install, SST drift detection on PR (sst diff), SST deploy on merge, SSM output verification, update-wiki.py status push.

Workflow generation

scripts/generate-workflows.py in bank-wiki reads every source/entities/modules/*.yaml, resolves the dependencies graph into a phase-ordered DAG (matching the build sequence in source/pages/delivery/build-sequence.md), and emits one workflow YAML file per module per repo. Within a phase, independent modules run as parallel jobs. Between phases, jobs are gated on the completion of all modules in the preceding phase.

The generator is re-run whenever compile.py produces a new module YAML. Generated files are committed to the code repos by the operator.

Cross-repo ordering (repository_dispatch)

scripts/update-wiki.py is extended (FR-736) to fire a repository_dispatch event to the GitHub repositories of all modules that declare the just-Built module as a dependency. The payload carries the module_id and the new build_status. The receiving repo's generated workflow re-evaluates its readiness gate and begins its own pipeline if all dependencies are now Built.

GitHub configuration

OIDC federation (FR-739)

All eight repositories use GitHub Actions OIDC to assume the AWS IAM role at /bank/{env}/iam/cicd/arn (provisioned by MOD-104). No long-lived AWS credentials are stored as GitHub Secrets. The OIDC trust policy is scoped to the specific repo and branch using the sub claim.

GitHub Environments (FR-737)

Three environments are configured on all eight repositories:

Environment Approval requirement
dev None — ungated
uat One approver from platform-leads team
prod Two approvers from platform-leads team

Branch protection (FR-738)

The main branch of every repository is protected: - All CI status checks defined in the generated workflow must pass - Minimum one approving human review required per PR - Stale review approvals dismissed on new commits - No direct push to main for any identity (including admins)

Policy compliance

DT-007 (GATE): the CI pipeline is the single mandatory path to any deployed environment. The generated workflows contain no bypass flags and no skip conditions.

DT-010 (AUTO): environments and protection rules are emitted from the generator and applied via GitHub API; no engineer manually configures an environment.

OPS-006 (LOG): GitHub Actions produces an immutable workflow run log per execution. update-wiki.py additionally writes a structured entry to the bank-wiki commit history on every status change, creating a cross-repository audit trail.

Outputs consumed by other modules

Output Purpose
bank-platform/.github/workflows/reusable-lambda.yml Called by every Lambda module workflow
bank-platform/.github/workflows/reusable-iac.yml Called by every IaC module workflow
{repo}/.github/workflows/mod-NNN-*.yml (generated) Per-module CI/CD pipeline

Constraints

  • Generated workflow files are the only permitted CI configuration for bank modules. Hand-written workflows that bypass the reusable templates are non-compliant with DT-010.
  • The generate-workflows.py script must be re-run and the generated files committed whenever a module's dependencies change in the wiki.
  • MOD-104 must be deployed before any generated workflow can authenticate. The generated workflows enforce this via the OIDC role lookup rather than hard-coded credentials.

Policies satisfied:

Policy Mode Description
DT-007 — Change and release management GATE No module may be deployed to any environment without the CI pipeline passing all mandated checks — no manual bypass path exists.
DT-010 — Environments and deployment standards AUTO GitLab Environments, protected branch rules, and OIDC role bindings are automatically provisioned from source-controlled config — no manual environment configuration is permitted.
OPS-006 — Change Management Policy LOG Every pipeline execution, approval gate decision, and environment promotion is logged as an immutable event linked to the triggering commit and approver identity.

MOD-157 — External provider stub service

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-157 provides Lambda stub functions for every external third-party provider used across the platform's eight system domains — identity verification, payment clearing networks, credit bureaus, and open banking connectors. It also deploys notification capture infrastructure for Amazon Pinpoint and a rules-based fraud model stub for MOD-023.

Without this module, integration tests for modules that call external APIs would either call real production endpoints (unsafe, expensive, non-deterministic) or require each module to implement its own mocking strategy (inconsistent and incomplete).

The compliance reason is DT-007: every module must pass integration tests against the dev environment before it is eligible for UAT promotion. Those integration tests cannot pass without realistic provider responses. MOD-157 makes that possible without regulatory exposure.

Architecture

consuming module Lambda
  ├── reads SSM: /{repo}/{stage}/provider/base-url
  │                       │
  │              dev/UAT: └──► MOD-157 API Gateway
  │                              └── stub Lambda handler
  │                                    └── DynamoDB (stub state)
  │                                    └── async: fires webhook callback
  └── prod: real provider URL from SSM

One API Gateway serves all stubs in a given stage. Routes are namespaced by provider category: - /oidv/* — eIDV providers (DVS, DIA, Onfido, Equifax, Centrix) - /sanctions/* — sanctions and PEP list downloads (MOD-013, MOD-014) - /clearing/npp/* — NPP real-time payments (AU) - /clearing/becs/* — BECS direct debit batch (AU) - /clearing/swift/* — SWIFT cross-border messages - /clearing/bpay/* — BPAY bill payments (AU) - /clearing/esas/* — ESAS real-time gross settlement (NZ) - /clearing/nzfp/* — NZ faster payments - /openbanking/akahu/* — Akahu open banking (NZ) - /openbanking/cdr/* — CDR open banking (AU) - /bureau/* — credit bureau enquiries (Equifax AU, Centrix NZ) - /post-sftp/* — Australia/NZ Post agency banking batch simulation - /notifications/capture — notification log query endpoint

SSM outputs

MOD-157 writes stub endpoint URLs to SSM at the paths each consuming module reads. The pattern is:

/{repo}/{stage}/{provider-category}/base-url  →  https://stubs.{stage}.{domain}/{category}

Consuming modules declare their expected SSM paths in their own docs/design/MOD-NNN.md. The reusable-iac.yml step verifies these paths exist after MOD-157 is deployed.

Test pattern convention

Stub responses are driven by patterns in request input data, not by configuration switches. This makes integration tests self-contained:

Provider Test pattern Response
eIDV (all) Document ref PASS-* Verified, high confidence
eIDV (all) Document ref FAIL-* Rejected — identity mismatch
eIDV (all) Document ref REFER-* Manual review required
Onfido Webhook fires 2 seconds after initial request
Sanctions Name contains SANCTIONED Confirmed match
Sanctions Name contains PEP- PEP hit, no sanctions
NPP Destination account ends 0001 Cleared, settlement confirmed
NPP Destination account ends 0002 Dishonoured — insufficient funds
NPP Destination account ends 0003 Timeout — no clearing response
BECS Payer BSB 062-000 All presentments honour
BECS Payer BSB 062-001 Second presentment dishonours
Bureau Date of birth 1900-01-01 No bureau record found
Bureau Date of birth 1900-01-02 Adverse record present

Async stub behaviour

For providers with async clearing lifecycles (Onfido, NPP, BECS, SWIFT, ESAS), the stub stores the pending request in DynamoDB and fires a webhook callback to the consuming module's registered callback URL after a configurable delay (default 2 seconds). The callback URL is read from SSM at /{repo}/{stage}/{provider}/callback-url, written by the consuming module at its own deploy time.

Notification capture

Amazon Pinpoint is used as a real AWS service in all environments. A notification capture Lambda is subscribed to the SNS topic that MOD-063 uses for dispatched messages. Every notification (type, recipient address, subject, body, timestamp) is written to a DynamoDB table notification-capture-{stage}. Integration tests query this table via the /notifications/capture endpoint to assert delivery.

Query pattern:

GET /notifications/capture?customer_id=CUST-001&type=email&after=2026-04-27T00:00:00Z

Fraud model stub

The fraud model artefact path is configured via SSM at /bank-payments/{stage}/fraud/model-s3-path. In dev and UAT, this path points to a stub model file deployed by MOD-157. The stub applies simple rules:

  • Amount > NZD/AUD 10,000 → score 0.9 (auto-decline threshold)
  • Payment reference contains FRAUD-TEST → score 0.9
  • All other payments → score 0.1 (pass)

MOD-023 code and configuration are unchanged; only the model artefact differs between environments.

Deployment scope

MOD-157 is deployed to dev and uat stages only. The sst.config.ts for this module conditionally skips all resource provisioning when stage === 'prod'. Running sst deploy --stage prod on this module is a no-op.

Policies satisfied:

(No policies assigned)


MOD-158 — Test seed data loader

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-158 loads deterministic seed data into the dev and UAT Neon databases on first deploy to a fresh stage. It ensures that every integration test has a realistic baseline to work against — customers with known identities, accounts with balances, chart of accounts, GL configuration, and service credentials for test runners.

Without this module, each team member standing up a new dev environment or the CI pipeline deploying to a fresh UAT stage would start with an empty database. Modules with integration tests that depend on pre-existing customers, accounts, or GL entries would fail immediately. Data loading would become a manual, inconsistent, and often forgotten step.

Architecture

The seed loader runs as an AWS Lambda function invoked by an SST Custom Resource during sst deploy. The deployment sequence is:

  1. SST deploys MOD-158 resources (Lambda function, IAM role, DynamoDB version table)
  2. SST Custom Resource triggers the seed Lambda
  3. Lambda checks /{repo}/{stage}/seed-version in SSM
  4. If current version is already recorded → exit immediately (idempotent)
  5. If not → load the seed profile for the stage, then write the version key

Re-running sst deploy does not reload seed data because the version key is already present. Resetting requires an explicit operator command (pnpm run seed:reset --stage {stage}), which truncates seed tables and removes the version key before re-running.

Seed profiles

dev profile (10 customers, ~20 accounts)

Customer Jurisdiction eIDV outcome Sanctions
CUST-D001 to CUST-D005 NZ PASS Clear
CUST-D006 to CUST-D008 AU PASS Clear
CUST-D009 NZ FAIL Clear
CUST-D010 NZ PASS HIT (SANCTIONED)

Each passing customer has one everyday account and one savings account. Balances range from NZD/AUD 2,000 to 50,000. All accounts are in Active state. Chart of accounts (100-series GL codes) and interest rate schedule (standard product rates) are also loaded.

A service account credential (scoped JWT) for integration test runners is written to Secrets Manager at /bank-{repo}/dev/test-runner/jwt.

uat profile (100 customers, ~250 accounts)

Superset of the dev profile, extended with: - 90 customers with realistic NZ and AU demographics (names, DOBs, addresses generated deterministically from seed 20260427) - Account types: everyday, savings, notice (30-day and 90-day), term deposit (3-month and 12-month) - 90 days of pre-loaded transaction history (bulk INSERT, not processed through the event pipeline) - Accounts in edge states: one dormant (last transaction > 24 months ago), one pending KYC review (CDD tier upgrade triggered), one restricted (sanctions hit pending adjudication) - Full product instance records (PRD-001 through PRD-005)

Neon branch mapping

Stage Neon branch Provisioned by Persists across deploy
dev dev MOD-103 Yes
uat uat MOD-103 Yes
prod main MOD-103 Yes

The UAT Neon branch is never torn down by SST. Accumulated synthetic transaction history (from MOD-159) persists until an explicit seed:reset is run. This is intentional — UAT should look like an operating bank with history.

Version key

The SSM key /{repo}/{stage}/seed-version holds the semver string of the last successfully loaded seed profile (e.g. 1.0.0). When the seed loader code changes in a way that requires a reload, the version constant in src/seed-version.ts is bumped. On the next deploy, the Lambda detects the version mismatch and reloads.

Patch bumps reload seed data. Minor bumps add new data without truncating. Major bumps truncate and reload from scratch.

Scope

MOD-158 is deployed to dev and uat stages only. Like MOD-157, the sst.config.ts skips all resource provisioning when stage === 'prod'.

Policies satisfied:

(No policies assigned)


MOD-159 — Synthetic transaction engine

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-159 generates synthetic banking activity on a schedule, making dev and UAT environments look and behave like a bank that is actively operating rather than a set of deployed modules against an empty database. It gives confidence that independently-deployed modules are continuously working together — if the engine runs successfully, the ledger, balance engine, account state machine, payment validation, audit trail, and notification modules are all functioning end-to-end.

The commercial reason is stakeholder and operator confidence. Any stakeholder logging into UAT on any given day should see accounts with recent transactions, current balances, and notification history — a realistic simulation of customer activity.

Architecture

EventBridge Scheduler
  └── triggers synthetic-transaction-generator Lambda
        ├── reads seed customer list from DynamoDB (written by MOD-158)
        ├── reads current balances from MOD-003 (Neon read-replica)
        ├── reads account states from MOD-007 (Neon)
        └── for each active account with sufficient balance:
              └── publishes PaymentInstructionRequested event to EventBridge
                    └── standard payment processing pipeline
                          MOD-020 (validation)
                          → MOD-001 (posting)
                          → MOD-003 (balance update)
                          → MOD-022 (audit trail)
                          → MOD-063 (notification)

The engine does not write to the database directly. All transactions flow through the same EventBridge events that real customer transactions use, exercising the full pipeline on every run.

Schedule and volume

Stage Schedule Runs/day Transactions/run Daily total
dev Tue and Thu, 09:00 NZST ~0.3 50 ~15 avg
uat 08:30 and 16:30 NZST 2 ~200 ~400
prod Disabled

Dev volume is deliberately light to avoid interfering with integration tests that assert specific account balances.

Transaction mix

Per run, the engine generates a realistic mix across the seed customer population:

Type Proportion Description
Retail payment (outbound) 40% Utilities, subscriptions, merchant payments
P2P transfer (intra-bank) 20% Customer to customer within the platform
Inbound credit 25% Simulated payroll (Friday runs), rental income, refunds
Savings transfer 10% Everyday → savings account sweep
Fee debit 5% Monthly account fee (triggered on first run of month)

Payroll credits are generated only on the Thursday or Friday schedule run nearest to the 15th and last day of the month.

Constraints

The engine enforces these rules before submitting any transaction:

  • Balance check: debit amount must not exceed 90% of current balance (leaves buffer for fees and interest)
  • Account state: only Active accounts receive transactions; Dormant, Restricted, and Pending accounts are skipped
  • Daily limit: no account receives more than 3 synthetic transactions per calendar day
  • Idempotency: a synthetic_run_id composed of date + account_id + sequence is embedded in the payment reference; MOD-001's idempotency key rejects duplicates if the engine runs twice in a day

Reproducibility

The random selections (amount, counterparty, transaction type) are seeded by SHA256(stage + ISO-date + customer_id), truncated to a uint32. This means the same customer generates the same transactions on the same day regardless of which Lambda invocation runs — useful for debugging mismatches between environments.

Activation gate

The engine's EventBridge schedule is created in DISABLED state. It is enabled via sst deploy only after all mandatory dependencies (MOD-001, MOD-003, MOD-007, MOD-020) are confirmed deployed in the target stage. The CI workflow for MOD-159 verifies these SSM output paths exist before enabling the schedule.

Policies satisfied:

(No policies assigned)


MOD-160 — Cross-module acceptance suite

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

Purpose

MOD-160 runs end-to-end acceptance scenarios that cross system domain boundaries, validating that independently-deployed modules work correctly together as a bank. It answers the question that per-module integration tests cannot: does the system as a whole do what it is supposed to do?

Per-module integration tests (in each module's tests/integration/) verify that a module behaves correctly given a correctly-configured environment. MOD-160 verifies that the modules compose correctly — that data and events flow across boundaries, that state transitions in one module are visible to modules that depend on them, and that the user-facing outcomes (account activated, payment processed, notification received) actually materialise.

Architecture

MOD-160 deploys a set of acceptance scenario Lambda functions, each implementing one end-to-end flow. An EventBridge schedule triggers a dispatcher Lambda nightly that invokes each scenario function in dependency order and aggregates pass/fail results.

EventBridge Scheduler (nightly, 02:00 NZST)
  └── dispatcher Lambda
        ├── invokes scenario-001-customer-onboarding
        ├── invokes scenario-002-account-activation
        ├── invokes scenario-003-inbound-payment
        ├── invokes scenario-004-outbound-payment
        ├── invokes scenario-005-daily-accrual
        └── ... (scenarios added as modules are deployed)
        └── aggregates results
              ├── CloudWatch custom metric: E2EAcceptanceSuitePass (1/0)
              ├── CloudWatch dashboard: bank-acceptance-{stage}
              └── SNS alert on failure → on-call channel

Scenarios that depend on modules not yet deployed are automatically skipped (the dispatcher checks the module's SSM output path before invoking the scenario). This means MOD-160 can be deployed early and the suite grows as the platform is built out.

Acceptance scenarios

Scenarios are added to this module as the platform reaches the phases where the relevant modules are built. The initial set covers Phase 1–4 modules:

Scenario Modules exercised Description
S-001 Customer onboarding MOD-009, MOD-010, MOD-013 Onboard a test customer using a PASS eIDV identity; assert kyc_status = Verified, CDD tier assigned, sanctions clear
S-002 Account activation MOD-007, MOD-009 Open an everyday account for a verified customer; assert state transitions from Pending to Active
S-003 Inbound credit MOD-001, MOD-003, MOD-063 Submit an inbound payment event; assert ledger entry created, balance updated, notification dispatched
S-004 Outbound payment MOD-020, MOD-001, MOD-003, MOD-022 Submit a payment instruction; assert validation passes, ledger posts, balance decrements, audit record created
S-005 Payment blocked — sanctions MOD-013, MOD-020 Submit payment for a SANCTIONED counterparty; assert MOD-020 rejects with sanctions failure code
S-006 Daily accrual MOD-005, MOD-001, MOD-003 Trigger daily accrual Lambda; assert interest posting appears in ledger and balance reflects
S-007 Notification capture MOD-063 Trigger a customer notification; query MOD-157 notification capture log; assert message delivered within 30 seconds
S-008 eIDV fail → account blocked MOD-009, MOD-007 Attempt onboarding with a FAIL eIDV identity; assert account remains in Pending state, not activated

Reporting

Results are reported at three levels:

  1. CloudWatch dashboard (bank-acceptance-{stage}): one row per scenario, pass/fail status, last run time, duration
  2. Custom metric E2EAcceptanceSuitePass (namespace BankPlatform/Acceptance): 1 if all enabled scenarios passed, 0 if any failed
  3. SNS alert: fires to bank-ops-{stage} topic on first failure of the night; suppresses repeat alerts for 6 hours

Failed scenario Lambda logs include the full assertion failure message and the relevant entity IDs (customer_id, account_id, payment_id) for immediate investigation.

Scope

MOD-160 is deployed to dev and uat stages. It is not deployed to prod — production monitoring is handled by the observability platform (MOD-076) and module-specific CloudWatch alarms, not synthetic acceptance tests.

Policies satisfied:

(No policies assigned)


MOD-168 — Maker-checker enforcement engine

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

What it does

MOD-168 is the platform-wide maker-checker enforcement engine. It provides a single shared service for all back-office modules that need four-eyes authorisation on consequential state-changing commands. Any module that needs a second person to approve an action before it executes delegates to MOD-168 rather than building its own proposal table, approval workflow, and audit trail.

The module exposes four APIs: submit a proposal, approve a proposal, reject a proposal, and query open proposals. It stores every proposal, decision, and expiry in an immutable log and writes each decision to MOD-047 (agent action logger) and MOD-048 (system decision log). No proposal may be self-approved — this constraint is enforced at the database layer and cannot be bypassed by any role, environment variable, or configuration flag.

Why it exists

Before ADR-059, maker-checker was implemented per-module. MOD-127 (product configuration panel) has its own app.product_config_proposals table. MOD-140 (chart of accounts) has its own core.gl_account_proposals table. Both were built correctly for their scope. The problem is that each new back-office module with write operations would independently implement the same pattern, creating fragmented audit surfaces — separate proposal tables in separate schemas with no central query, no cross-module approval queue, and no single view for compliance reporting.

ADR-059 resolved this by establishing MOD-168 as the single enforcement point. New modules built after ADR-059 must call MOD-168 rather than creating their own proposal tables. Existing implementations (MOD-127, MOD-140) remain in place as v1 and are flagged for migration to MOD-168 in their respective v2s.

Command classification

Every command that registers with MOD-168 is classified at one of two risk tiers. The owning module declares the tier when it registers its command type. The classification register is reviewed and approved by the compliance team.

TIER-2 — Medium risk, reversible. Product configuration changes, fee schedule updates, interest rate adjustments, payment limit overrides. MOD-168 requires a second approver before execution but applies no review window — the approver may act immediately after the proposal is submitted.

TIER-3 — High risk, irreversible or regulatory impact. Sanctions holds, account closures, GL account creation and modification, credit limit overrides below policy floor, customer risk rating downgrades, hardship arrangement applications. MOD-168 requires a second approver AND enforces a minimum 15-minute review window between proposal submission and approval. This window gives the reviewing officer time to investigate context before committing. The review window is configurable per command type via AppConfig.

API surface

POST   /checker/proposals             Submit a pending command for review
GET    /checker/proposals             List open proposals (paginated; filterable by domain, command_type, tier, status)
GET    /checker/proposals/{id}        Retrieve a single proposal with full context
POST   /checker/proposals/{id}/approve   Second-party approval (reviewer ≠ proposer enforced)
POST   /checker/proposals/{id}/reject    Second-party rejection with mandatory reason

The proposal payload carries the full command context — the command type, the target entity, the proposed new state, a human-readable summary, and the proposer's staff ID. The approver supplies only a confirmation and an optional note. The command context is stored verbatim in the proposal record and cannot be altered after submission.

Data model

CREATE TABLE platform.checker_proposals (
  id                 uuid          PRIMARY KEY DEFAULT gen_random_uuid(),
  command_type       text          NOT NULL,              -- e.g. 'PRODUCT_RATE_CHANGE', 'GL_ACCOUNT_CREATE', 'SANCTIONS_HOLD'
  command_domain     text          NOT NULL,              -- owning system domain, e.g. 'SD01', 'SD02', 'SD07'
  tier               text          NOT NULL CHECK (tier IN ('TIER-2', 'TIER-3')),
  status             text          NOT NULL DEFAULT 'PENDING'
                                   CHECK (status IN ('PENDING', 'APPROVED', 'REJECTED', 'EXPIRED', 'WITHDRAWN')),
  target_entity_type text          NOT NULL,              -- e.g. 'product', 'gl_account', 'party'
  target_entity_id   text          NOT NULL,              -- entity primary key
  command_payload    jsonb         NOT NULL,              -- full command context; immutable after insert
  summary            text          NOT NULL,              -- human-readable one-line description for the approval queue
  proposed_by        text          NOT NULL,              -- staff_id of the proposing officer
  proposed_at        timestamptz   NOT NULL DEFAULT now(),
  review_window_until timestamptz  ,                      -- earliest approval time for TIER-3; NULL for TIER-2
  reviewed_by        text          ,                      -- staff_id of the reviewer; set on APPROVED or REJECTED
  reviewed_at        timestamptz   ,
  review_note        text          ,                      -- optional reviewer note
  expires_at         timestamptz   NOT NULL,              -- proposal expires if not actioned (configurable per command_type, default 48h)
  jurisdiction       char(2)       NOT NULL CHECK (jurisdiction IN ('NZ', 'AU')),
  trace_id           uuid          NOT NULL,
  idempotency_key    text          NOT NULL UNIQUE,
  created_at         timestamptz   NOT NULL DEFAULT now()
);

DB-level invariant: CHECK (proposed_by != reviewed_by) — enforced at the database layer. No role or configuration can override this constraint. This is the unconditional self-approval block.

Immutability: command_payload, proposed_by, proposed_at, and tier are set at INSERT and never updated. The status field transitions PENDING → APPROVED / REJECTED / EXPIRED / WITHDRAWN via a Cat 2 trigger that allows only these forward transitions and prevents any reversal.

Indexes: - idx_checker_proposals_status on (status, expires_at) WHERE status = 'PENDING' - idx_checker_proposals_proposed_by on (proposed_by, proposed_at DESC) - idx_checker_proposals_command_domain on (command_domain, status, proposed_at DESC) - idx_checker_proposals_target on (target_entity_type, target_entity_id) WHERE status = 'PENDING'

Integration with MOD-062 (TIER-3 review window)

For TIER-3 commands, MOD-168 uses a Step Functions task-token pause/resume pattern (MOD-062 FR-295): the proposal Lambda submits the command to a Step Functions state machine, then suspends. The state machine holds a task token. When the reviewer approves via the MOD-168 approve API, MOD-168 resumes the Step Functions execution with the task token. If the review window has not elapsed, the approve API returns 422 REVIEW_WINDOW_NOT_ELAPSED. If the proposal expires before approval, the state machine times out and records an EXPIRED status. TIER-2 commands do not use Step Functions — the approve API directly executes the command and writes the APPROVED record synchronously.

Approval queue for back-office operators

The MOD-168 query API powers the unified approval queue in the back-office app (SD08). Operators with checker role see all PENDING proposals across all command domains they are authorised to review. The queue is filterable by domain, tier, command type, and age. Each proposal shows the full command context, the proposed-by officer, the time since submission, and (for TIER-3) the review window countdown.

Self-approval block test requirement

Every module that integrates with MOD-168 must include a policy test (tests/policy/self-approval-blocked.ts) that verifies the 422 SELF_APPROVAL_FORBIDDEN response when the same staff ID attempts to both submit and approve a proposal. This test must pass in CI before any integration with MOD-168 is considered complete.

Migration path for MOD-127 and MOD-140

MOD-127 and MOD-140 each maintain their own proposal tables and approval workflows as v1 implementations. In their respective v2 builds, the proposal table is deprecated and the approval flow is replaced with MOD-168 API calls. The MOD-168 command type for product configuration changes is PRODUCT_RATE_CHANGE / PRODUCT_TERM_CHANGE / PRODUCT_FEE_CHANGE; for GL account changes it is GL_ACCOUNT_CREATE / GL_ACCOUNT_MODIFY. Historical proposal records from the v1 tables are archived to cold storage before the v1 tables are dropped.

Policies satisfied:

Policy Mode Description
GOV-003 — Three Lines of Defence Policy CHECKER The enforcement engine is the platform-wide implementation of the three-lines-of-defence second-line control — every consequential back-office command above TIER-1 requires a second authorised reviewer before execution, with self-approval blocked at the database layer.
DT-012 — Ledger Data Contracts & Event Publication Policy LOG Every proposal, approval, rejection, and expiry is written to the system decision log (MOD-048) and the agent action logger (MOD-047) as an immutable audit record, satisfying the ledger data contracts and event publication audit obligation.
GOV-005 — Financial Accountability Regime (FAR) Policy CHECKER FAR accountability obligations require that material operational decisions are attributable to named individuals — the maker-checker record provides the named proposer and named approver for every TIER-2 and TIER-3 command.

MOD-176 — Snowflake read API service

System: SD07 | Repo: bank-platform | Build status: Deployed | Deployed: Yes

The Snowflake read API service is the governed gateway between application services and Snowflake Tier 3 data (ADR-038). It is the concrete implementation of the "Snowflake read API" service mandated by ADR-038 and is built in bank-platform (SD07) because the auth, circuit-breaking, warehouse management, and query governance concerns are platform-wide, not scoped to any single system domain.

Two access patterns

Tier 3a — Customer-facing signals

Pre-shaped presentation table point lookups scoped to the requesting customer's party_id. These serve the customer mobile/web app via bank-app. Queries are trivial indexed lookups against purpose-built presentation tables refreshed by Snowflake Dynamic Tables.

  • Warehouse: Dedicated XS warehouse, always-on during business hours, 10-minute auto-suspend on inactivity
  • Latency target: ≤500ms p99
  • Scoping: Every query is bound to the authenticated party_id — the API rejects any query whose result set would span multiple customers regardless of parameters passed
  • Endpoint: GET /v1/snowflake/signals/{party_id}/{signal_type}

Tier 3b — Back-office and regulatory queries

Structured metric queries forwarded to the Snowflake Cortex Analyst REST API. These serve MOD-177 (SD06 risk dashboard renderer) and internal back-office tooling. Callers send structured metric requests {metric, groupBy, filters}; Cortex Analyst generates and executes the SQL against the SD06 modules' published semantic views.

  • Warehouse: Dedicated back-office warehouse, auto-suspend after inactivity
  • Latency target: ≤5 s p95 acceptable for back-office use
  • Scoping: Role-scoped via MOD-044 RBAC — Snowflake row access policies enforce the same restriction at the data layer as defence-in-depth
  • Endpoint: POST /v1/snowflake/metrics

Semantic view ownership

CREATE SEMANTIC VIEW DDL is authored and maintained by each SD06 module in its own migrations directory, alongside dbt models. MOD-176 has no knowledge of view structure — it is a proxy. Cortex Analyst resolves metric names against the semantic models registered in the Snowflake account. This preserves the schema-as-product contract (ADR-046): SD06 modules own their data and metric definitions; SD07 owns the query proxy and access governance.

Operational constraints

  • Circuit breaker (Tier 3a): If query latency exceeds 1 second p95, the API returns a structured degraded response ({available: false, reason: "snowflake_latency"}). Slow Snowflake has zero impact on the transaction path.
  • Circuit breaker (Tier 3b): 10-second timeout; structured error response on breach.
  • Query governance: Every query is logged with caller identity, query type, metric name or signal type, warehouse, query duration, and Snowflake query ID — for cost attribution and anomaly detection via MOD-076.
  • No cache: Presentation table freshness (Tier 3a) is managed by Dynamic Table refresh cadence. Tier 3b results are not cached — Cortex Analyst latency on the back-office warehouse is acceptable.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE All inbound queries pass through TLS-terminated, JWT-authenticated endpoints — no Snowflake credentials are exposed to calling services or the browser.
DT-002 — Cybersecurity Policy GATE Per-caller rate limiting and RBAC role scoping enforced at the API layer — unauthenticated or out-of-scope queries are rejected before reaching Snowflake.

SD08 — Customer App & Back Office Platform

Repo: bank-app | Business domain: BD01 | Tech owner: Product Engineering | Build status: Not started

The customer-facing mobile/web application and the back office agent platform — both served from a single codebase with mode-aware rendering.

Modules

ID Name Status ADR
MOD-049 Consent capture module Not started ADR-004, ADR-019
MOD-050 Disclosure enforcement module Not started ADR-004, ADR-012
MOD-051 Financial automation rules engine Not started ADR-018, ADR-020
MOD-052 Role-scoped data access Not started ADR-004
MOD-053 Case & complaint management Not started ADR-011
MOD-054 Call recording & transcript attachment Not started ADR-019

For full module specifications and acceptance criteria, see module specifications.

Architecture

See ADR-004 for the single-codebase dual-mode design decision and the back office superset pattern.

Critical constraints

  1. MOD-049 consent capture is a hard GATE — no product feature may be activated without recorded customer consent.
  2. MOD-050 disclosures must be rendered before any credit or investment product is offered.
  3. MOD-052 role-scoped access must enforce the principle of least privilege — back office agents see only what their role requires.
  4. MOD-054 call recordings must be stored in compliance with PRI-001 and CON-007 retention requirements.

Modules in SD08


System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Purpose

The open banking consent management module is the single store of record for all open banking consents, regardless of jurisdiction. It implements the consent lifecycle — capture, validation, refresh, amendment, and revocation — in a jurisdiction-neutral way, with each active jurisdiction profile contributing its own consent schema. The consent store is the authoritative source for MOD-061 (gateway) and MOD-084 (data recipient) to validate whether a given data access is authorised.

What it does

A consent is a first-class entity: consent_id, jurisdiction_profile [au_cdr/nz_payments_nz/uk_open_banking/eu_psd2/generic_fapi2], customer_id, third_party_id, third_party_name, granted_scopes (list of internal scope identifiers), granted_at, expires_at (nullable — some profiles allow perpetual consents), status [active/revoked/expired/suspended], consent_payload (JSONB — the full jurisdiction-specific consent representation, e.g. CDR arrangement, OBIE permissions object, PSD2 authorization details).

Internal scopes are profile-agnostic identifiers (e.g. accounts:read, transactions:read:90d, payees:read) mapped from each profile's native permission model. The gateway uses internal scopes for enforcement; profile-specific serialisation happens at the API layer.

app.ob_consents stores the canonical consent record. app.ob_consent_events stores the full audit trail: created, amended, refreshed, revoked (by customer or TPP), expired, suspended.

AU CDR: consent captured via CDR authorisation flow — arrangement ID, data cluster selection, sharing period. Consumer must confirm at each renewal.

NZ (Payments NZ): consent via API Centre authorisation code flow — permission set, duration, purpose statement.

UK Open Banking: consent via OBIE authorisation flow — permission codes, transaction from/to dates, expiry date.

EU PSD2: SCA + dynamic linking for payment initiation; account information consent via ASPSP authorisation.

Generic FAPI 2.0: scope-based consent with RAR (Rich Authorization Requests) — consent detail in the authorization_details JWT claim.

Synchronous validation endpoint used by MOD-061 on every inbound TPP request: given (third_party_id, customer_id, requested_scopes, jurisdiction_profile), returns {valid: true/false, reason?, expires_at} within 50 ms.

Revocation propagates within 60 seconds — revoked consents return valid: false immediately after revocation is recorded.

Customers can view all active consents from the in-app settings screen: TPP name, profile, granted scopes (human-readable), expiry, and a single-tap revoke button.

Revocation is immediate — the TPP receives a 403 on their next request with no grace period.

Compliance reason

Every open banking regime requires that data sharing is customer-authorised and that the authorisation is specific, time-limited, and revocable. A single consent store that understands all profile schemas means a customer who revokes consent for an AU CDR arrangement and a UK Open Banking consent is handled by the same revocation logic, same audit trail, and same 60-second propagation guarantee — regardless of which regulator is looking.

Commercial reason

A unified consent store eliminates the need to build and maintain separate consent databases per jurisdiction. Customer-facing consent management (view, amend, revoke) is built once and works across all active profiles.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy GATE requireConsent() workspace library throws 403 CONSENT_NOT_GRANTED if no active row in app.consents (or app.ob_consents for OB purposes) exists for the customer + purpose tuple — personal data access is blocked without prior consent.
CON-007 — Consumer Data Right (CDR) Policy GATE OB consent grant endpoint rejects any payload that does not carry the jurisdiction profile's required fields (CDR arrangement_id, OBIE permission codes, etc.) per per-profile Ajv JSON Schema validation — non-compliant CDR consent attempts are rejected before any row is inserted.
AML-010 — AML Training & Awareness Policy LOG app.staff_training_acks stores immutable, timestamped AML/CFT training completion records per staff member — gated by MOD-052 so only authenticated staff may write their own acknowledgement; retained for seven years under ADR-048 Cat 1 immutability.

MOD-050 — Disclosure enforcement module

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Purpose

Enforce all regulated pre-acceptance disclosure obligations as hard gates that the customer must pass before a product activates, a payment is submitted, or a fee-generating action completes. Each disclosure event is logged with content version and acknowledgement timestamp for regulatory examination. The module also generates the NZ DTA Key Information Summary (KIS) for deposit products, satisfying the disclosure obligations of the NZ Deposit Takers Act 2023 Disclosure Standard.

What it does

Pre-acceptance disclosure gate

Every product acceptance, payment confirmation, or fee-generating action is blocked at the platform layer until the customer acknowledges the required disclosure. The gate is enforced at the service layer, not the UI — it cannot be bypassed by a client application, API caller, or back-office operator.

Disclosure types handled:

  • Product terms and conditions — full product agreement presented and acknowledged before account or loan activation (CON-004)
  • Responsible lending disclosure — total repayment amount, total cost of credit, and effective annual rate shown before any loan product is accepted (CRE-002)
  • Foreign exchange rate and spread — live rate and spread shown and acknowledged before any cross-border transfer is submitted; rate is locked for 30 seconds after acknowledgement (PAY-004)
  • Fee disclosure — itemised fee shown before any fee-generating action; the action cannot proceed until the customer acknowledges the fee (CON-005)

Each disclosure event is recorded in the system decision log (MOD-048) with: disclosure type, content version hash, presenting channel, customer session ID, and acknowledgement timestamp. Content versions are immutable once published — if disclosure content changes, a new version is created and all new disclosures use the new version.

NZ DTA Key Information Summary (KIS)

For deposit products opened by customers of NZ-licensed deposit takers, the module generates a Key Information Summary in the format prescribed by the RBNZ DTA Disclosure Standard. The KIS is a standardised one-page document in plain language covering:

  • Product name and type
  • Interest rate (or rate range for variable products), rate type (fixed/variable/tiered), and interest calculation method
  • Key fees — account keeping fee, transaction fees, early exit fee (if applicable)
  • Minimum and maximum balance requirements
  • Access restrictions (notice period for notice accounts, term for term deposits)
  • DCS coverage eligibility statement (linked to MOD-142 data)
  • Contact details and complaints pathway

The KIS is generated from the product's configuration record in MOD-127 using a templated renderer. The template is maintained by the platform and updated when the RBNZ finalises or amends the Disclosure Standard.

KIS gate: the KIS is presented to the customer during account opening via MOD-050's standard disclosure gate. The deposit product cannot activate until the customer has acknowledged the KIS. Acknowledgement is recorded with the KIS version hash in app.dcs_fcs_disclosures.

Persistent access: after opening, the current KIS for each of the customer's deposit products is accessible at any time from the account detail screen. When a KIS is updated (product terms or fee change), the customer receives a notification via MOD-063 and must acknowledge the updated KIS within 30 days; failure to acknowledge does not block account access but is flagged for follow-up.

Version control: each KIS version is a content-addressed record — kis_version_id is derived from a hash of the KIS content fields. Any change to a product's configuration that affects KIS content triggers a new version. Old versions are retained for audit purposes; no version is ever deleted.

The DTA Disclosure Standard is currently in Draft. The platform builds to the known consultation paper requirements. When the standard is finalised, the KIS template may require updates; these are delivered as a platform update without requiring a schema migration.

Compliance reason

NZ CCCFA and AU NCCP both require responsible lending disclosure before credit is extended — MOD-050 enforces this with no bypass path. The DTA Disclosure Standard will require every NZ deposit taker to deliver a KIS in prescribed format before any deposit account is opened; banks are building to the Draft now to avoid last-minute compliance delivery. The hard gate model — enforcement at the service layer, not the UI — means disclosure compliance is guaranteed regardless of which channel or client application a customer uses to open an account.

Commercial reason

Pre-acceptance disclosure done well reduces post-sale complaints and hardship applications by ensuring customers genuinely understand what they are agreeing to. Automated KIS generation from product configuration eliminates manual document production for each product launch or fee change — the compliance document is always in sync with the actual product terms.

Policies satisfied:

Policy Mode Description
CON-004 — Product Disclosure & Sales Practice Policy GATE Disclosure obligation met before every product acceptance — system enforces, no agent required
CRE-002 — Responsible Lending Policy GATE Responsible lending disclosure — repayment amount and total cost shown before loan acceptance
PAY-004 — Cross-Border Payments & FX Policy GATE FX rate and spread shown and acknowledged before cross-border transfer executed
CON-005 — Fee & Pricing Transparency Policy GATE Fee disclosure shown before any fee-generating action — no surprise fees
CON-009 — NZ DTA Key Information Summary Disclosure Policy GATE NZ DTA Key Information Summary generated in RBNZ-prescribed format and acknowledged by the customer before any deposit product activates — NZ deployments only; KIS version and acknowledgement timestamp logged.

MOD-051 — Financial automation rules engine

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Customer-configurable rules (sweep, round-up, rate alert) executed automatically. Rules stored as structured JSON in Postgres. Kafka consumer evaluates rules on transaction events. See ADR-018.

Policies satisfied:

Policy Mode Description
CON-001 — Customer Fairness & Conduct Policy AUTO Automated actions executed exactly as customer configured — no discretionary deviation
PAY-001 — Payment Operations Policy GATE Automated payments subject to same pre-payment validation as manual payments
CON-005 — Fee & Pricing Transparency Policy LOG Rule execution logged and visible to customer — full transparency of automated actions

MOD-052 — Role-scoped data access

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

All data returned by the API is scoped to the authenticated agent's role. Enforced server-side, not client-side. See ADR-004.

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE Minimum necessary data access enforced at API — no role can access data outside their scope
PRI-001 — Privacy Policy AUTO Personal data access limited to authorised roles — principle of minimum necessary enforced
AML-006 — Suspicious Activity Reporting Policy AUTO SAR data accessible only to compliance and legal roles — segregation enforced at data layer

MOD-053 — Case & complaint management module

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Full IDR case lifecycle — creation, assignment, SLA tracking, escalation, FSCL/AFCA referral. SLA breaches trigger automatic escalation.

Case classes

MOD-053 manages two distinct case classes, each with its own queue, SLA, and routing rules.

IDR — Internal dispute resolution

Customer complaints and disputes following the IDR process required under NZ FMA and AU ASIC RG 271. Covers billing disputes, service complaints, and product complaints. FSCL (NZ) / AFCA (AU) referral when IDR does not resolve within the statutory timeframe.

AML-REFER — Acceptance engine referral

Triggered when MOD-153 (customer acceptance engine) produces a REFER or HOLD_FOR_EDD outcome on a product application. A compliance officer must review the application against the full party risk profile and make a manual ACCEPT, DECLINE, or escalate-to-EDD decision.

Event trigger: MOD-053 subscribes to bank.kyc.acceptance_decided on the bank-kyc EventBridge bus, filtered to detail.decision IN ['REFER', 'HOLD_FOR_EDD'].

SLA: Case must be created within 60 seconds of decision_at (FR-713). If creation exceeds 60 seconds, a CloudWatch alarm fires to the compliance team.

Case payload: party_id, product_id, reason_codes, triggered_rules, decision_at, and the full rule trace from MOD-153's event detail.

Adverse-action notice: When the compliance officer's decision is DECLINE, MOD-053 publishes a decision event consumed by MOD-063 (notification orchestration), which generates the adverse-action notice within 24 hours per CCCFA s.9B (NZ) and NCCP s.130 (AU).

Note: The v1 SNS stub that MOD-153 shipped to the MOD-076 alarm-intake topic is the interim path while this consumer rule is in flight. Once the MOD-053 consumer rule is live, MOD-153's stub is removed (follow-up to bank-kyc).

Policies satisfied:

Policy Mode Description
CON-002 — Complaints & Internal Dispute Resolution Policy ALERT IDR SLAs enforced automatically — agent cannot ignore a case past SLA without triggering escalation
CON-003 — Vulnerable Customer Policy AUTO Vulnerable customer flags visible in every agent view — special handling applied automatically
REP-001 — Regulatory Reporting Policy LOG Complaint register maintained automatically — feeds regulatory complaints report

MOD-054 — Call recording & transcript attachment

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Every agent call recorded with consent gate, transcribed in real time (AWS Transcribe), PII redacted, AI-summarised by Snowflake Cortex within 2 minutes, and attached to CRM record automatically. See ADR-019.

Policies satisfied:

Policy Mode Description
CON-004 — Product Disclosure & Sales Practice Policy LOG Product disclosures in calls captured and searchable — compliance QA can evidence verbal disclosure
PPL-003 — Training & Competency Policy AUTO Agent training and quality scored automatically from call transcripts — no manual QA sampling required
GOV-006 — Internal Audit Policy LOG Call corpus provides audit evidence for sales practice and conduct reviews

MOD-064 — Operations work queue

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

The operations work queue is the daily workspace for bank operations staff. It aggregates items requiring human action from across the platform — payment holds, AML alerts, loan referrals, escrow release approvals, KYC exceptions, and complaint escalations — into prioritised queues organised by function and role.

Each item displays the full context: customer identity, product, amount, the automated decision or rule that triggered the item, and the action required. Staff work through their queue in the app; all actions (approve, decline, escalate, comment) are captured with operator identity and timestamp. The module enforces CAP-090 multi-step approval requirements: items that need dual authorisation remain in the queue until both approvers have signed off.

Replaces ad-hoc email and spreadsheet-based operations processes with a consistent, auditable workflow surface. SLA management is built in — items approaching their deadline are automatically escalated.

Policies satisfied:

Policy Mode Description
GOV-002 — Risk Appetite Statement Policy AUTO Routes every decision that requires human review to a role-appropriate queue — no manual triage needed.

MOD-068 — Authentication & session management

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Authentication and session management is the security boundary between the internet and all customer-facing and operator-facing surfaces of the platform. It handles the full ceremony of proving identity — biometric gesture, passkey assertion, MFA challenge — and converts a successful proof into a scoped, time-limited session token that all downstream modules rely on.

The module maintains a device registry: each device used to access the platform is fingerprinted and assigned a trust level on first use after a full authentication ceremony. Recognised trusted devices can use biometric-only login; new or suspicious devices are challenged with additional factors. Step-up authentication is triggered automatically by the payment initiation and operations modules when a high-risk action is requested — the user is re-challenged in-flow before the action proceeds.

Session tokens are short-lived and silently refreshed in the background; the module revokes all active sessions for a customer on logout, password change, or when the fraud scorer raises a suspicious-activity flag. Designed against FIDO2 / WebAuthn standards with phishing-resistant credentials as the primary factor.

Policies satisfied:

Policy Mode Description
DT-002 — Cybersecurity Policy GATE Enforces multi-factor authentication and device trust checks as a prerequisite for session establishment — no session is issued without passing cybersecurity controls.
PRI-001 — Privacy Policy GATE Access to customer data requires a valid, unrevoked session tied to a verified identity — no anonymous data access is permitted.

MOD-069 — Customer app shell

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

The customer app shell is the outermost container of the bank's mobile and web applications. It owns the navigation graph, screen routing, deep link handling, theming, and the lifecycle hooks that all product feature modules plug into. Without it the individual feature modules (payments, accounts, profile, etc.) have no consistent surface to render in.

The shell evaluates feature flags at launch and on session establishment, showing or hiding features based on entitlement, cohort, or phased rollout configuration. This allows product teams to deploy code to production and activate features for specific user groups without a new release — supporting A/B tests, early access programmes, and jurisdiction-specific feature sets.

Disclosure and regulatory notice gates are managed at the shell level: flows that require a disclosure to be presented before proceeding (fee disclosure, CDR consent) are enforced here rather than in individual modules, ensuring no path through the app can bypass a required regulatory step.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy AUTO Displays fee disclosures and regulatory notices in the correct position within user flows — disclosure gates are enforced by the shell before navigation proceeds.

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Transaction history and search provides the customer's view of their financial activity across all accounts and currencies. It reads from the immutable transaction log and real-time balance engine, enriches each entry with merchant name, logo, and category data from the enrichment model, and presents the result in a paginated, searchable list.

The module provides full-text search across merchant names and transaction descriptions, with filters for date range, amount, category, and account. Results load progressively so the customer sees the most recent activity immediately while older data pages in. Tapping an entry shows the full detail: merchant, original currency amount, exchange rate applied, fee if any, and the running balance at that point.

Export produces a formatted PDF (bank statement style) or CSV of any filtered view, suitable for tax records, expense claims, or visa applications. All export requests are logged against the customer's session for audit purposes.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy AUTO Customers can view a complete, accurate transaction history with fee and FX detail for every entry — no charges are hidden or obscured.
GOV-006 — Internal Audit Policy LOG Customer-facing transaction history is derived from the immutable ledger — any access or export request is logged.

MOD-071 — Payment initiation

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Payment initiation is the customer-facing entry point for all outgoing payment types: domestic transfers, international wires, BPAY, scheduled payments, and direct debit management. It presents a consistent initiation experience across payment types while routing each submission to the appropriate backend processing module.

Two-step flow (PAY-001)

All payments follow a preview→confirm two-step sequence. POST /payments/preview assembles a ValidatePaymentRequest payload, calls MOD-050 for any required FX disclosure, evaluates step-up requirements, and returns a preview_id with the full fee and FX breakdown. POST /payments/confirm accepts the preview_id plus an optional step_up_token; it will not proceed without a valid preview_id (422 if missing or expired). Direct submission paths are rejected. A negative test asserts that confirm-without-preview returns 422.

app.payment_previews rows expire after 15 minutes. The preview_id is the public reference throughout the flow.

FX and fee transparency (CON-005)

The preview response always carries fee_amount, fx_rate, fx_markup, base_currency_amount, and total_to_debit. The confirmation screen surfaces all of these before the customer taps confirm. For FX payments (INTERNATIONAL_WIRE), MOD-050 is called at preview time for a FX_TERMS_AND_CONDITIONS disclosure acknowledgement gate. If MOD-050 returns "not required" (disclosure type not yet catalogued), the flow proceeds — this is the v1 posture; the disclosure type will be added in a follow-up MOD-050 amendment.

Step-up authentication (FR-355)

Step-up is required for two conditions: (a) payment amount exceeds the configured threshold (default NZD/AUD 500, env-var STEP_UP_THRESHOLD_DEFAULT; customer-configurable deferred to v1.1); (b) the matched payee in app.payees has first_use_completed = false. When either condition applies, step_up_required = true is returned on the preview response. The confirm handler invokes MOD-068 via Lambda invoke (STEP_UP_FN_ARN injected from SSM path /bank/{env}/mod068/step-up/fn-arn) to verify the x-step-up-token header. first_use_completed is set to true on the payee row after a successful first payment.

Payment submission

On confirm: MOD-071 calls POST /internal/v1/payments/validate on MOD-020 synchronously, passing the pre-minted payment_id (minted at preview creation — same pattern as MOD-141/MOD-119/MOD-120/MOD-122) and the idempotency_key derived as sha256(preview_id::text || ':' || party_id::text). MOD-020 creates the canonical payments.payments row with the pre-minted payment_id. SigV4 signing uses BankAppRole. If MOD-020's resource policy has not yet been extended to BankAppRole, a handoff is filed to bank-platform — same file-if-needed approach as MOD-070. MOD-071 does not publish any EventBridge events; MOD-020 publishes bank.payments.payment_initiated as the canonical payment signal.

Scheduled payments

app.scheduled_payments stores one-off and recurring payment instructions. The scheduled-payment-sweeper Lambda runs daily at 07:00 NZST (cron(0 19 ? * * *)). For each ACTIVE row with next_run_at <= now(), the sweeper mints a fresh payment_id, derives a per-run idempotency key (idempotency_prefix || ':' || run_count), calls MOD-020, writes a RESULTED event (carrying MOD-020's decision), and updates next_run_at (recurring) or sets status = 'COMPLETED' (one-off). OSKO is excluded from scheduled payments — it is a real-time rail not suited to future-dating.

Direct debit management (CAP-009)

v1: GET /direct-debits returns [] with a deferral note. DD mandate management is owned by MOD-114 (bank-payments). The live mandate feed will be wired in v2.

Data model

Four tables in the app schema. Full schemas in SD08 data model. Key points:

  • app.payment_previews — mutable, 15-min TTL; one row per initiation session; holds the pre-minted payment_id and full disclosure fields
  • app.payment_initiation_events — Cat 1 immutable (INSERT only); one row per state transition; carries session_id, device_fingerprint, mod_020_payment_id, mod_020_decision, trace_id
  • app.payees — mutable address book; soft-deleted; first_use_completed drives FR-355 gate
  • app.scheduled_payments — mutable instruction store; next_run_at index for sweeper

Policies satisfied:

Policy Mode Description
PAY-001 — Payment Operations Policy GATE Presents the full payment details for customer confirmation before submission — no payment is initiated without explicit customer authorisation.
CON-005 — Fee & Pricing Transparency Policy AUTO Displays the applicable FX rate, markup, and fee clearly on the confirmation screen before the customer commits to a payment.

MOD-072 — Customer profile & settings

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Customer profile and settings is the self-service interface for managing the non-transactional aspects of the customer's relationship with the bank. It covers personal details (name, address, contact information), security settings (trusted devices, session history, 2FA preferences), notification preferences, language, and linked external accounts.

Changes to regulated fields — email address, phone number, residential address — are gated behind a re-verification step: the customer must confirm their identity before a change takes effect. The previous value is retained and an alert is sent to the old contact point on any change, providing a defence against account takeover. Changes are logged with timestamp and session identity for audit purposes.

The module surfaces data held by the bank about the customer in plain language, fulfilling the access and correction rights obligations under the Privacy Act (NZ) and Privacy Act 1988 (AU). Customers can view their profile data, submit a correction request, and track its status without calling the contact centre.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy AUTO Customers can view and correct all personal information held about them — the profile module provides a self-service interface for data accuracy rights.
CON-001 — Customer Fairness & Conduct Policy AUTO All profile changes are confirmed and acknowledged by the customer before being applied — no silent updates to contact or identity details.

MOD-073 — Document vault

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

The document vault is the secure store for all documents associated with a customer's relationship with the bank — identity documents uploaded at onboarding, signed loan contracts, trade finance instruments, KYC refresh evidence, and statements. It provides the customer-facing upload and download interface and the internal storage and retrieval API used by other modules.

All documents are stored encrypted at rest with per-document encryption keys managed by the secrets module. Access is scoped to the owning customer and to bank staff with an active, authorised reason — any staff access is logged with the accessing user's identity, role, and stated reason. Customers can view a list of all documents held about them, fulfilling the subject access right under NZ and AU privacy law.

Statement generation is on-demand: the customer selects an account and date range, and the vault generates a PDF formatted as an official bank statement with a tamper-evident hash. Statements produced are logged and can be verified by third parties (e.g. mortgage lenders) against the hash. Retention schedules are enforced automatically — documents past their mandated retention period are purged without manual intervention.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy GATE Customer documents are stored with access controls scoped to the owning customer and authorised bank staff only — no cross-customer document access is permitted.
PRI-003 — Personal Information Retention & Destruction Policy AUTO Documents are retained for the required regulatory period and purged automatically when retention expires — the vault enforces the retention schedule.

MOD-074 — Back-office customer 360

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

The back-office customer 360 view is the primary workspace for bank operations, compliance, and support staff when working on a specific customer. It aggregates data from across the platform — identity and KYC status, all accounts and balances, transaction history, credit profile, risk scores, open cases, recent communications, and document vault — into a single screen, eliminating the need to navigate multiple back-end systems during a customer interaction.

The view is read-only by default; specific action tools are available to operators whose role grants the relevant permission. Available actions include updating account limits, changing account state (freeze, block, close), overriding an automated decision with a documented reason, and adding case notes. All actions are gated by the role-scoped access module and logged immutably — the audit trail shows exactly what was viewed, what was changed, and why.

Designed to reduce average handle time for customer support calls and compliance reviews: the operator sees everything relevant on a single screen within two seconds of searching by customer name, email, phone, or account number. The view is also used by the AML and fraud teams during alert investigation.

Build notes

has_postgres: false — MOD-074 owns no Postgres tables and ships no Flyway migrations. All data is read from published views in the consolidated Neon DB (ADR-064); all access audit is delegated to MOD-047 via staff.action_taken on the bank-platform EventBridge bus.

Customer search — implemented as a direct SQL query against kyc.party_search_view in the consolidated Neon DB. No inter-service API hop. Requires GRANT SELECT ON kyc.party_search_view TO app_readonly (tracked in issue #32; blocks deployment).

AI summary (FR-368) — return {is_available: false, reason: "MOD-083 not yet deployed"} until MOD-083 reaches Deployed status.

staff.action_taken — publish to bank-platform EventBridge bus reusing the cross-bus grant established by MOD-053. No new IAM grant required.

Performance — aggregate all data sources in parallel within a single Lambda invocation; p99 ≤2 s per FR-365; no caching in v1.

Field masking matrix

Role × data section access for the customer 360 view. Roles correspond to cognito:groups claims defined in MOD-044 / MOD-052; exact Cognito group names are implementation constants in MOD-052. Implement as constants in the MOD-052 enforcement library.

Data section customer-support operations compliance senior
Full name, DOB, nationality Full Full Full Full
Government ID (NZ IRD / AU TFN) Last 4 only Last 4 only Full Full
Contact (email, phone, address) Full Full Full Full
Account number Last 4 only Full Full Full
Balances & transaction summary Full Full Full Full
KYC status & CDD tier Read Read Full Full
Risk score & flags Hidden Read Full Full
AML cases & open alerts Hidden Hidden Full Full
SAR data Hidden Hidden Full (compliance / legal only) Full
Credit profile (limits, arrears) Hidden Read Read Full
Document vault Read Read Full Full
Action — add case note Allowed Allowed Allowed Allowed
Action — update account limits Forbidden Allowed Forbidden Allowed
Action — change account state Forbidden Allowed Forbidden Allowed
Action — override CDD decision Forbidden Forbidden Allowed Allowed

Hidden = field not returned in API response (not masked with placeholder). Read = displayed read-only, no edit. Full = displayed with full value, editable where the action permission is granted.

SAR data visibility is also subject to AML-006 (GATE, MOD-052) — the compliance.officer and legal.officer Cognito groups only.

Cross-schema read dependencies

MOD-074 reads from published views across four schemas. All four require GRANT SELECT to app_readonly before first deployment (tracked in GitLab issue #32, to::bank-platform):

Schema View / table Owning system Status
kyc party_search_view SD02 / bank-kyc Pending grant (issue #32)
kyc cdd_tier_assignments SD02 / bank-kyc Pending grant (issue #32)
kyc party_regulatory_profiles SD02 / bank-kyc Pending grant (issue #32)
banking customer_relationships SD01 / bank-core Pending grant (issue #32)
banking customer_contact_readable SD01 / bank-core Pending grant (issue #32)

Policies satisfied:

Policy Mode Description
GOV-002 — Risk Appetite Statement Policy LOG All back-office access to customer data and all manual actions taken on customer accounts are logged with operator identity and timestamp.
PRI-003 — Personal Information Retention & Destruction Policy GATE Back-office access to customer records requires an active authorised session with a role that includes customer data access — no anonymous or unscoped access.

MOD-077 — Account dashboard & insight feed

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

The account dashboard and insight feed is the home screen of the customer app — the first thing a customer sees on every open. It aggregates and presents the customer's complete financial picture: all account balances, accrued interest, a 14-day predicted cash flow view, and spending trends for the current period. The data is pre-computed by the analytics and risk platform modules (MOD-039, MOD-040, MOD-041) and read on load — no live analytical query runs when the customer opens the app.

The insight card section surfaces contextual signals derived from the customer's account behaviour: idle cash sitting below its potential yield, a pre-approved credit offer based on the latest eligibility score, an unusual spend pattern worth reviewing, or an FX rate signal if the customer holds multiple currencies. Each card includes a one-tap action — move to savings, check loan terms, review the transaction, initiate a transfer. The cards are ranked by relevance score from the analytics layer and refresh within 60 seconds of a qualifying event (a large transaction, a rate change, a new pre-approval result).

Balance visibility is real time: the dashboard reads from the live balance engine (MOD-003), so the balance shown is always post-settlement. Net worth includes linked external accounts (consented via CDR) alongside internal accounts, giving customers a single number for their full financial position across institutions.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy AUTO Displays account balances, accrued interest, and fee information accurately and in real time — the dashboard is the customer's primary source of financial truth.

MOD-078 — Card & account controls

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Card and account controls is the self-service panel for customers who need to act on their account immediately — freeze a lost card, tighten a spending limit before a trip, generate a virtual card for a one-off online purchase, or refresh their KYC documents. All actions in this module route to backend enforcement modules (MOD-021 for card controls, MOD-010 for KYC) and take effect in real time without any contact centre involvement.

The card freeze control is the most time-critical feature in the module: a customer who suspects their card is lost or compromised can freeze it in a single tap from the home screen shortcut or from the card detail screen. The freeze is applied to the payment authorisation engine within seconds, blocking all new card-present and card-not-present transactions while leaving existing direct debits unaffected. Unfreeze is equally immediate. A card replacement request flows directly from the freeze confirmation screen.

Spending limits allow the customer to set category-level controls (e.g. gambling blocked, contactless capped at NZD 200 per transaction) and an overall daily limit below the system maximum. Virtual card generation produces a unique 16-digit number usable for online purchases, reducing exposure of the physical card number. The KYC refresh flow guides the customer through re-submitting identity documents and completing a liveness check when a periodic review is due, replacing the legacy model of sending documents by post or visiting a branch.

Policies satisfied:

Policy Mode Description
PAY-005 — Payment Fraud Prevention Policy GATE Card freeze executed immediately from the app removes a compromised card from the fraud attack surface without delay — no call centre required.
CON-001 — Customer Fairness & Conduct Policy AUTO Spending limits and card controls are set and visible to the customer in the app — changes take effect in real time with immediate confirmation.

MOD-083 — Agent assist & compliance coaching panel

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

The agent assist and compliance coaching panel is a real-time compliance overlay embedded in the back-office interface that surfaces policy obligations, disclosure scripts, and behavioural prompts to operators during customer interactions.

Purpose

Back-office operators handling inbound calls, live chats, and case escalations must navigate complex disclosure requirements, IDR complaint procedures, vulnerable customer protocols, and product suitability rules simultaneously. Without real-time guidance, compliance relies on training recall — creating a risk of disclosure omission, incorrect product advice, or missed vulnerability indicators. This panel makes the relevant obligation visible at the moment it applies.

What it does

  • Contextual compliance prompts — as the operator navigates the customer 360 view (MOD-074), the panel reads the interaction context (product being discussed, complaint flag, vulnerability markers, channel) and surfaces the relevant policy obligations from the compliance engine
  • Disclosure scripts — for interactions involving fee disclosure (CON-005), responsible lending disclosure (CRE-002), or FX spread disclosure (PAY-004), the panel provides the required disclosure language that the operator must deliver — with a confirmation checkbox before proceeding
  • IDR workflow prompts — when an interaction is flagged as a complaint (CON-002), the panel displays the IDR SLA countdown, required acknowledgement steps, and escalation paths; operators cannot close a complaint interaction without confirming the disclosure steps
  • Vulnerability indicators — when the customer record carries a vulnerability flag (CON-003), the panel surfaces the required special handling protocol
  • Coaching log — every prompt surfaced, dismissed, or confirmed is logged against the interaction record for training quality review and regulatory evidence

What it does not do

This panel does not make automated compliance decisions. It surfaces obligations and records confirmation — the operator remains responsible for the interaction. Automated policy enforcement (account restrictions, payment blocks) is handled by the respective transaction-path modules.

Policies satisfied:

Policy Mode Description
CON-002 — Complaints & Internal Dispute Resolution Policy AUTO IDR complaint obligations surfaced to the agent in real time during a customer interaction — script prompts ensure required disclosures are not omitted.
GOV-007 — Conflicts of Interest Policy AUTO Compliance coaching nudges are logged against the interaction record — training and coaching evidence is available for regulatory review.

MOD-090 — Auto rules engine

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

What it does

MOD-090 is the auto rules engine. It stores and applies user-defined rules and implicitly learned rules that override or confirm the base classification from MOD-088.

Rule types

Explicit rules — created directly by the customer: - "This merchant is always a business expense for me" - "Uber rides between 8–9am on weekdays are personal commute (non-claimable)" - "All transactions over $200 at hardware stores are property expenses"

Implicit rules — learned from user corrections: - If a user consistently reclassifies a given merchant from "Personal" to "Business", MOD-090 generates an implicit rule after a configurable number of confirmations and applies it to future transactions automatically, pending a one-time user review of the inferred rule.

Rule priority

Rules are applied in priority order: explicit rules override implicit rules, which override the base model output from MOD-088. All overrides are logged with the rule ID and basis so the customer can audit why a transaction was classified as it was.

Feedback loop

Confirmed and corrected classifications from MOD-090 are fed back to MOD-088 as labelled training examples. This is the primary mechanism by which the classification model personalises over time.

Design phase

This module is in design. Build begins in Phase 2 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG User-defined and inferred classification rules constitute personal financial data; rule creation, modification, and application events are logged for data minimisation audits and individual access requests.

MOD-091 — Receipt processor

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

What it does

MOD-091 is the receipt processor. It ingests receipt images from two sources — camera capture in the customer app, and email forwarding — and attaches the extracted data to the corresponding transaction record.

Ingestion paths

App capture — customer photographs a receipt immediately after purchase. The image is uploaded from the app and queued for OCR processing.

Email ingestion — customer forwards a receipt email to a dedicated bank email address. MOD-091 parses the email (text and HTML), extracts the receipt data, and attempts to match it to a transaction.

OCR and extraction

Receipt images are processed using a document OCR pipeline (vendor TBD during Phase 1 build evaluation — candidates include AWS Textract, Google Document AI). Extracted fields:

  • Merchant name
  • Transaction date and time
  • Total amount (and per-item amounts where available)
  • GST amount (where shown separately on the receipt)
  • Payment method (for cross-reference validation)

Transaction matching

Extracted receipt data is matched to a candidate transaction by: 1. Amount (exact match within rounding tolerance) 2. Date (within a configurable window, default ±3 days) 3. Merchant name similarity (fuzzy match against enriched merchant name from MOD-087)

A confidence score is computed for the match. Above the high-confidence threshold the receipt is automatically attached. Below threshold, the customer is prompted to confirm or select from candidate transactions.

Design phase

This module is in design. Build begins in Phase 2 of the Expense Intelligence Platform. See the Expense Intelligence Platform summary for the full implementation roadmap.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy LOG Receipt images and extracted data are classified as personal financial data; retention and access are logged per PRI-001.

MOD-108 — Product offer engine

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

What it does

MOD-108 generates bank-initiated personalised product offers. It operates on the eligible and NBP-ranked product set and derives specific offer terms (interest rate, credit limit, fee structure, promotional bonus) personalised to each customer. Offers are stored with full lifecycle tracking: GENERATED → PRESENTED → ACCEPTED / REJECTED / EXPIRED.

Distinguish clearly from MOD-109 (product deal engine): MOD-108 is systemic and bank-initiated — offers are generated by the engine without a triggering agent interaction. MOD-109 is agent-initiated — a specific deal is proposed by an agent during a customer interaction and requires authorisation.

Why it exists

Personalised offers convert at significantly higher rates than generic rate-card offers. The commercial case (BG-003, BG-005) is conversion improvement through personalisation. The compliance case is that every offer must be traceable — what was offered, to whom, on what basis, and what happened to it — satisfying CON-004 and CON-006.

Offer derivation

Input Source Effect on offer terms
NBP rank MOD-107 Rank 1 product generates the primary offer; ranks 2–3 generate secondary offers
Customer ROTE MOD-106 High-ROTE customers eligible for premium rates; low-ROTE customers offered standard terms
Behavioural consent MOD-049 If marketing consent present: personalised rate. If not: standard rate-card offer only
Pre-approval limit MOD-029 For credit products: offer limit derived from pre-approval result; never exceeds affordability max
Product rate card Product configuration Floor and ceiling for offer rates; offer cannot exceed ceiling or fall below floor

Financial advice licensing gate

Before generating an offer for any product flagged requires_advice_review = true in the product register, MOD-108 checks that an authorised adviser has reviewed the customer profile within the past 12 months (read from product_eligibility.advice_reviews). If no review exists, the offer is suppressed and an offer.advice_review_required event is raised for the back-office queue. Currently no products in the bank's register require this gate, but the gate is built and active — it will be triggered when investment or KiwiSaver distribution products are added.

Offer lifecycle

GENERATED
  → PRESENTED (displayed in app or surfaced to agent)
    → ACCEPTED  (customer accepted offer terms; triggers application workflow)
    → REJECTED  (customer declined)
    → EXPIRED   (offer validity period elapsed — default 30 days for credit; 14 days for rate offers)
  → SUPPRESSED (advice review required; or eligibility re-evaluation failed before presentation)

Data model

-- app.product_offers (Postgres — bank_app)
CREATE TABLE app.product_offers (
  offer_id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  party_id            uuid NOT NULL,
  product_id          text NOT NULL,
  jurisdiction        text NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  offer_terms         jsonb NOT NULL,         -- rate, limit, fee waiver, promotional bonus
  derivation_basis    jsonb NOT NULL,         -- snapshot of inputs used to derive terms
  status              text NOT NULL CHECK (status IN ('GENERATED','PRESENTED','ACCEPTED','REJECTED','EXPIRED','SUPPRESSED')),
  nbp_rank            int,
  offer_type          text NOT NULL CHECK (offer_type IN ('RATE','LIMIT','FEE_WAIVER','PROMOTIONAL')),
  valid_from          timestamptz NOT NULL DEFAULT now(),
  valid_to            timestamptz NOT NULL,
  presented_at        timestamptz,
  responded_at        timestamptz,
  response            text,                   -- ACCEPTED / REJECTED / null
  created_at          timestamptz NOT NULL DEFAULT now()
  -- append-only: no UPDATE/DELETE; status changes are new rows via offer_events
);

-- app.offer_events (append-only lifecycle log)
CREATE TABLE app.offer_events (
  event_id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  offer_id    uuid NOT NULL REFERENCES app.product_offers(offer_id),
  event_type  text NOT NULL,   -- GENERATED, PRESENTED, ACCEPTED, REJECTED, EXPIRED, SUPPRESSED
  actor       text NOT NULL,   -- 'system' or staff_id
  event_at    timestamptz NOT NULL DEFAULT now(),
  detail      jsonb
);

Events

  • product.offer_presented — on every app presentation; carries party_id, product_id, offer_id, jurisdiction.
  • product.offer_accepted — triggers application or account-open workflow in the relevant system domain.
  • product.offer_expired — nightly sweep; triggers NBP re-evaluation for the customer.

Policies satisfied:

Policy Mode Description
CON-006 — Product suitability and governance GATE No offer is generated for a product that is not in the customer's eligible set from MOD-105 — eligibility check is a hard pre-condition for offer generation.
CON-004 — Product Disclosure & Sales Practice Policy LOG Every generated offer is logged with its terms, derivation basis, and lifecycle events (presented, accepted, rejected, expired) in an immutable offer audit trail.
CON-001 — Customer Fairness & Conduct Policy AUTO Offer generation rate and acceptance outcomes are monitored by MOD-107 fairness reporting — systematic disparities trigger a compliance alert.
PRI-001 — Privacy Policy GATE Behavioural personalisation of offer terms requires the customer's active consent record for data-driven marketing — no behavioural offer is generated without a valid consent.

MOD-109 — Product deal engine

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

What it does

MOD-109 manages agent-initiated bespoke product deals. When an agent is speaking with a customer (via call, chat, or in-branch) and determines that a personalised arrangement would retain or acquire the customer, they propose specific deal terms through the back-office UI (MOD-083). MOD-109 validates those terms, routes them through the appropriate authorisation tier, presents the accepted deal to the customer, and logs the full lifecycle immutably.

Deal vs offer distinction

  • MOD-108 product offer — system-initiated; generated without agent involvement; terms derived algorithmically; no authorisation workflow; sent to customer via app or automated channel.
  • MOD-109 product deal — agent-initiated; triggered by a direct customer interaction; terms may include bespoke rate reductions, fee waivers, or enhanced limits; requires authorisation if outside the agent's self-approval tolerance.

Why it exists

Direct customer interactions are the bank's highest-conversion sales channel. When a retention conversation is in progress, the agent needs the ability to propose a competitive deal in real time — without a multi-day approval process that causes the customer to leave. MOD-109 provides the authorisation workflow and audit trail that make this operationally possible while maintaining the governance and consistent-treatment obligations of CON-006 and CON-001.

Authorisation tiers

Three tiers with configurable tolerance bands:

Tier Who Rate reduction tolerance Fee waiver tolerance Limit increase tolerance
1 — Agent self-approve Frontline agent ≤ 25 bps ≤ $500 ≤ $2,000
2 — Team manager Team manager (back-office role) ≤ 50 bps ≤ $2,000 ≤ $10,000
3 — Product/pricing committee Senior approval (product governance role) Any Any Any

All tolerance values are configurable in the deal_authorisation_config table — the tier structure is fixed but the numbers are IaC-managed.

Deal lifecycle

PROPOSED (agent submits deal terms)
  → SELF_APPROVED (within Tier 1 tolerance — proceeds immediately to PRESENTED)
  → PENDING_APPROVAL (exceeds Tier 1 — routed to Tier 2 approver queue)
    → APPROVED (Tier 2 approver confirms)
    → REFERRED (Tier 2 refers to Tier 3 product/pricing committee)
      → APPROVED
      → DECLINED (deal outside policy; agent notified with reason)
  → PRESENTED (approved deal terms communicated to customer)
    → ACCEPTED (customer agrees; triggers application or account modification)
    → REJECTED (customer declines)
    → EXPIRED (customer did not respond within validity window — default 48 hours for deals)

Data model

-- app.product_deals (Postgres — bank_app)
CREATE TABLE app.product_deals (
  deal_id               uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  party_id              uuid NOT NULL,
  product_id            text NOT NULL,
  jurisdiction          text NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  proposed_terms        jsonb NOT NULL,        -- rate, limit, fee waiver as proposed
  approved_terms        jsonb,                 -- may differ if approver modifies terms
  approval_tier         int NOT NULL CHECK (approval_tier IN (1,2,3)),
  proposed_by           text NOT NULL,         -- staff_id of proposing agent
  approved_by           text,                  -- staff_id of approver (null if tier 1 self-approved)
  approval_rationale    text,
  status                text NOT NULL CHECK (status IN (
                          'PROPOSED','PENDING_APPROVAL','APPROVED','DECLINED',
                          'PRESENTED','ACCEPTED','REJECTED','EXPIRED')),
  interaction_id        uuid,                  -- links to MOD-054 call or chat session
  proposed_at           timestamptz NOT NULL DEFAULT now(),
  approved_at           timestamptz,
  presented_at          timestamptz,
  responded_at          timestamptz,
  expires_at            timestamptz NOT NULL,
  created_at            timestamptz NOT NULL DEFAULT now()
);

-- app.deal_events (append-only lifecycle log)
CREATE TABLE app.deal_events (
  event_id    uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  deal_id     uuid NOT NULL REFERENCES app.product_deals(deal_id),
  event_type  text NOT NULL,
  actor_id    text NOT NULL,   -- staff_id or 'system'
  event_at    timestamptz NOT NULL DEFAULT now(),
  detail      jsonb
);

Events

  • product.deal_approved — on Tier 1 self-approval or Tier 2/3 approval; triggers presentation to customer.
  • product.deal_accepted — customer accepts; triggers account modification or new application workflow in the relevant system domain.
  • product.deal_declined — approval tier declined; carries decline reason for agent feedback.

Policies satisfied:

Policy Mode Description
CON-006 — Product suitability and governance GATE Agent-proposed deal terms are validated against product floor/ceiling rules and eligibility constraints before being presented to the customer — no deal outside configured tolerance can be authorised without the required approval tier.
CON-004 — Product Disclosure & Sales Practice Policy LOG Every deal proposal, authorisation decision, and customer acceptance or rejection is logged immutably in the deal audit trail with the agent ID, authoriser ID, terms, and timestamp.
CON-001 — Customer Fairness & Conduct Policy LOG Deal audit trail enables periodic review for consistent treatment — agents cannot offer materially different deals to similarly situated customers without an auditable reason.

MOD-113 — Statement generation

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

What it does

MOD-113 generates account statements for all deposit and credit accounts at the close of each statement period (monthly for transaction and savings accounts; monthly for credit products; at maturity for term deposits). Statements are delivered in-app and available as PDF download for 7 years. A paper statement option is available at a fee configured in the tenant fee schedule.

Statement contents by product type

Deposit accounts (transaction, savings): opening balance, all credits and debits with merchant name/description and timestamp, closing balance, interest earned for period, fees charged for period with itemisation, average balance for period.

Credit accounts (personal loan, revolving credit): opening balance, all transactions, payments received, interest charged (with rate applied and calculation basis), fees charged, closing balance, minimum payment due, payment due date, overdue amount if any.

Term deposits: account number, term, opening date, maturity date, principal, interest rate, interest earned to date, projected maturity proceeds.

Statement delivery

Statements are pushed to the customer's in-app document vault (MOD-073) on the day after statement period close. A push notification (MOD-063) is sent when the statement is available. Email delivery of PDF is available where the customer has enabled it. Paper statements are generated and mailed by an external print provider via API — the module produces a print-ready PDF and calls the print provider API; the provider handles physical delivery.

Regulatory retention

All statements are retained for 7 years in S3 (KMS encrypted) regardless of whether the account is closed. Customers can download any statement within the 7-year window at any time.

Re-generation

Statements can be re-generated by an authorised agent where a correction is needed (e.g. a fee reversal after statement close). The corrected statement is issued as a supplementary statement — it does not replace the original; both are retained.

Policies satisfied:

Policy Mode Description
CON-004 — Product Disclosure & Sales Practice Policy AUTO Account statements are generated and delivered automatically at the end of each statement period — no manual trigger; no customer is missed.
CON-005 — Fee & Pricing Transparency Policy AUTO Statements include all fees, interest, and charges for the period with clear disclosure of basis and amounts.

MOD-126 — Power of attorney and third-party authority

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Manages the registration, management, and revocation of formal and informal third-party authority arrangements on customer accounts. Supports three authority types:

  1. Power of Attorney (PoA) — a general authority granted by the account holder while they have legal capacity. Takes effect immediately on registration.
  2. Enduring Power of Attorney (EPoA) — a statutory instrument that continues or activates when the grantor loses capacity. May be registered while dormant and activated later on evidence of incapacity.
  3. Informal / limited third-party authority — a bank-level authority granted by the customer for a defined scope (e.g. a family member can make deposits but not withdrawals). Not a statutory instrument; governed solely by the bank's terms and the customer's written consent.

All three types require the appointed person to pass KYC via MOD-009 before being granted any access to the account.

Regulatory context

PoA and EPoA are statutory instruments and their validity is governed by legislation. In NZ: the Protection of Personal and Property Rights Act 1988 governs enduring powers of attorney for property. In AU: legislation varies by state — Powers of Attorney Act 1998 (Qld), Powers of Attorney Act 2014 (Vic), and equivalents in other states. The bank must accept validly executed PoA documents and cannot require customers to use proprietary bank forms.

ASIC and the FMA are both scrutinising banks' management of third-party authorities as a conduct risk and vulnerable customer issue. Poorly supervised PoA is a well-documented vector for elder financial abuse. Regulatory expectations include: clear audit trails for all attorney transactions, proactive monitoring for abuse patterns, and staff training to identify signs of coercion or exploitation at the point of registration.

Conduct risk

CON-003 (Vulnerable Customer Policy) treats EPoA activation as a signal that the account holder may be losing or have lost capacity. The abuse monitoring in this module is a first-line preventive control — not a reactive fraud detection tool. Attorney abuse (using PoA to self-deal or deplete an account) is one of the fastest-growing categories of financial harm in both NZ and AU. The module's weekly pattern analysis is designed to surface suspicious changes in attorney behaviour before significant financial harm occurs.

Authority types and scope

Type Granted by Activated when Scope configurable Revocable
PoA (general) Account holder Immediately on registration Yes Yes, by grantor at any time
EPoA (property) Account holder On incapacity (or immediately if specified in document) Limited Yes, by grantor before incapacity; court order required after
Third-party authority Account holder Immediately on registration Yes (view / deposit / transact) Yes, by grantor at any time

Scope levels:

  • view — read-only access to balances and transaction history.
  • deposit — can add funds; cannot withdraw or transfer.
  • transact — can initiate and approve transactions within the account holder's existing limits.
  • full — all capabilities including limit changes and account settings; used for EPoA when fully activated.

Data model

-- app.third_party_authorities
CREATE TABLE app.third_party_authorities (
  authority_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id          UUID NOT NULL REFERENCES core.accounts(account_id),
  grantor_customer_id UUID NOT NULL,
  grantee_customer_id UUID NOT NULL,  -- must have a verified KYC record
  authority_type      TEXT NOT NULL CHECK (authority_type IN ('poa','epoa','third_party')),
  scope               TEXT NOT NULL CHECK (scope IN ('view','deposit','transact','full')),
  document_reference  UUID,  -- MOD-073 document ID
  status              TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('pending_kyc','active','suspended','revoked')),
  granted_at          TIMESTAMPTZ,
  valid_from          DATE,
  valid_until         DATE,  -- null = indefinite
  activated_at        TIMESTAMPTZ,  -- for EPoA: timestamp when incapacity was confirmed
  revoked_at          TIMESTAMPTZ,
  revocation_reason   TEXT,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- app.authority_transactions
CREATE TABLE app.authority_transactions (
  tx_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  authority_id       UUID NOT NULL REFERENCES app.third_party_authorities(authority_id),
  transaction_type   TEXT NOT NULL,
  amount             NUMERIC(18,2),
  performed_at       TIMESTAMPTZ NOT NULL,
  channel            TEXT NOT NULL CHECK (channel IN ('app','back_office','branch')),
  created_at         TIMESTAMPTZ NOT NULL DEFAULT now()
);

authority_transactions provides the raw record of what an attorney did and when. MOD-047 provides the system-wide agent action log that cross-references these records for compliance review.

Key operations

Registration

The account holder initiates registration via the app or in-branch. They select the authority type and scope, and identify the grantee. If the grantee is not an existing customer, they complete eIDV via MOD-009. If the grantee is an existing customer, their KYC status is verified before proceeding.

For PoA and EPoA: the original document is uploaded to MOD-073. Back-office review is required to confirm the document is validly executed before the authority is activated. Status is pending_kyc until the grantee's KYC check passes, then moves to active once document review is approved.

For informal third-party authority: no statutory document is required, but the customer's written consent is recorded and stored in MOD-073. Activation is automatic once the grantee's eIDV is confirmed.

EPoA activation

An EPoA registered as dormant (to activate only on incapacity) remains in active status with scope restricted to view until activation. Activation is triggered by the grantee submitting evidence of the grantor's incapacity — typically a medical certificate from a registered practitioner or a court order — via the back-office channel.

A back-office agent reviews the documentation. Activation requires supervisor sign-off (two-person authorisation). On activation: activated_at is set, scope is elevated to the level specified in the PoA document, and the grantor is notified if there is any contact address on record.

Activation is an irreversible step absent a court order. The audit trail records the reviewing agent, approving supervisor, document reference, and timestamp.

Transaction tagging

All transactions initiated under a third-party authority are tagged at the point of initiation with the authority_id. This tag flows through to the transaction record in the core ledger and to MOD-047's agent action log, recording the grantee_customer_id as the acting party. This creates a complete, queryable audit trail distinguishing the account holder's own transactions from those made by an attorney or authorised third party.

Regulators and back-office investigators can therefore produce a full history of attorney actions on any account, including amounts, timing, and channel, without needing to reconstruct it from narrative notes.

Abuse monitoring

A weekly background job runs for all accounts with active PoA or EPoA authorities. For each authority, the job calculates:

  • Transaction frequency in the past 30 days vs the prior 3-month baseline frequency.
  • Total outgoing transaction value in the past 30 days vs the prior 3-month baseline value.

If either metric has increased by more than 3× relative to baseline, the job emits a bank.app.authority_abuse_alert event. A case is created in MOD-053 with case_type: potential_attorney_abuse. The back-office vulnerable customer team is notified via MOD-063 for manual review.

The 3× threshold is configurable via a feature flag. The default is intentionally sensitive — false positives are preferred over missed abuse. Back-office staff close false-positive cases with a documented reason.

Revocation

For PoA and third-party authorities: the grantor revokes via the app. Revocation is immediate — status = revoked, revoked_at is set, and the grantee's access is removed from all channels in the same transaction. The grantee is notified of the revocation.

For EPoA after incapacity is confirmed: the grantor cannot self-revoke. Revocation requires either a written revocation instrument executed by the grantor before incapacity was confirmed (if the timing is unambiguous) or a court order. Court order documents are stored in MOD-073. Back-office activation of court-order revocation requires the same two-person authorisation as EPoA activation.

Requirements

FR-569 — Authority registration with KYC gate: no third-party authority may be set to active status until the grantee holds a verified KYC record; the system must enforce this at the data layer, not only in application logic.

FR-570 — EPoA activation workflow: EPoA activation must require submission of incapacity evidence to MOD-073, back-office review, and supervisor sign-off; activation without all three steps must be technically blocked.

FR-571 — Transaction tagging under authority: every transaction initiated by an attorney or authorised third party must carry the authority_id in the transaction record and be logged to MOD-047 with the grantee's identity; untagged attorney transactions must be treated as a system error.

FR-572 — Abuse monitoring: the weekly monitoring job must run for all accounts with active PoA or EPoA authorities; detection thresholds must be configurable; alerts must create a MOD-053 case within 24 hours of detection.

Policies satisfied:

Policy Mode Description
CON-003 — Vulnerable Customer Policy AUTO Third-party authorities are monitored for abuse patterns — a sudden increase in transaction frequency or value by an attorney triggers an alert to the back-office team for review as a potential vulnerable customer concern.
AML-002 — Customer Due Diligence (CDD) Policy GATE Any new third-party authority — whether PoA, EPoA, or informal authority — requires a KYC check on the appointed person before they can transact on the account.
PRI-001 — Privacy Policy GATE The account holder's consent is required before any third-party authority is registered — except where a court order provides the authority directly.
GOV-006 — Internal Audit Policy LOG All third-party authority registrations, amendments, revocations, and transactions made under authority are logged immutably to the audit trail.

MOD-127 — Product configuration panel

System: SD08 | Repo: bank-app | Build status: Deployed | Deployed: Yes

Purpose

Provides back-office and product management staff with a governed interface for configuring product terms — interest rates, fee schedules, eligibility thresholds, and product-level feature flags. All changes require a four-eyes (maker/checker) approval before taking effect and are subject to a disclosure gate that prevents unfavourable changes reaching customers before required notification has been dispatched.

Compliance rationale

Under the Fair Dealing provisions of the Financial Markets Conduct Act (NZ) and the Australian Securities and Investments Commission Act, banks must provide customers with prior notice of changes to key terms and conditions — typically 14 days minimum for retail customers. This module makes compliance with that obligation a technical constraint, not a process obligation: an unfavourable rate or fee change simply cannot take effect until MOD-063 confirms that all affected customers have received the required notification.

Two-person authorisation is a standard operational risk control required under bank prudential standards (RBNZ/APRA). The maker/checker constraint prevents a single operator — whether acting negligently or maliciously — from making unauthorised changes to product configuration that could affect customer charges, eligibility criteria, or rate terms.

Commercial rationale

Product managers need agility to respond to competitive rate movements and regulatory changes. A governed self-service panel — rather than ticketed engineering changes — lets product, compliance, and pricing teams manage configuration within a controlled environment without requiring developer involvement for routine adjustments.

Configuration scope

This module controls the following configurable parameters per product (identified by product_id and jurisdiction):

Parameter type Examples
Interest rates Variable base rate, fixed rate tiers, introductory rate, overdraft rate
Fee schedules Monthly fee, transaction fee, overdraft facility fee, late payment fee
Product thresholds Minimum balance, maximum balance, LVR cap, income threshold
Notification periods Advance notice days before unfavourable change takes effect
Feature flags Joint accounts enabled, overdraft enabled, cheque account enabled

Parameters not in this scope: credit policy rules (managed in MOD-029), fraud scoring thresholds (managed in MOD-023), AML rules (managed in MOD-034). Those modules have separate governed configuration interfaces.

Data model

-- app.product_config_proposals
CREATE TABLE app.product_config_proposals (
  proposal_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  product_id        TEXT NOT NULL,
  jurisdiction      TEXT NOT NULL CHECK (jurisdiction IN ('NZ','AU','NZ + AU')),
  parameter_key     TEXT NOT NULL,
  current_value     JSONB,
  proposed_value    JSONB NOT NULL,
  change_reason     TEXT NOT NULL,
  proposed_by       UUID NOT NULL,  -- staff member ID
  proposed_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
  status            TEXT NOT NULL DEFAULT 'pending'
                    CHECK (status IN ('pending','approved','rejected','superseded','live')),
  reviewed_by       UUID,           -- must differ from proposed_by
  reviewed_at       TIMESTAMPTZ,
  review_comment    TEXT,
  effective_date    DATE NOT NULL,
  notification_required BOOLEAN NOT NULL DEFAULT false,
  notification_confirmed_at TIMESTAMPTZ,  -- set by MOD-063 callback
  applied_at        TIMESTAMPTZ
);

The proposed_by ≠ reviewed_by constraint is enforced at the database layer via a CHECK constraint on the table, not only in application code. This means no application-layer bypass can circumvent the four-eyes rule.

notification_required is set to true automatically when proposed_value represents an unfavourable change relative to current_value (higher fee, lower deposit rate, higher lending rate, stricter eligibility). The system determines "unfavourable" based on parameter-type metadata registered at startup.

Proposal and approval workflow

1. Propose. A product manager or pricing analyst logs in to the back-office panel and creates a proposal: selects the product, jurisdiction, parameter, enters the proposed value, effective date, and change reason. The system calculates whether notification is required and sets notification_required accordingly. The proposer submits.

2. Review. A second authorised staff member — who cannot be the proposer — reviews the proposal. They can approve, reject, or return for revision. On approval, status = approved and reviewed_by / reviewed_at are set.

3. Notification dispatch (if required). If notification_required = true, the system immediately calls MOD-063 to dispatch the advance notice to all affected customers. MOD-063 confirms dispatch via a callback that sets notification_confirmed_at. The proposal is held at approved until the callback is received.

4. Effective date gate. Even after notification is confirmed, the configuration change does not take effect until effective_date. A scheduled job runs daily and applies all approved proposals where effective_date <= today and either notification_required = false or notification_confirmed_at IS NOT NULL.

5. Application. On application, status = live, applied_at is set, and the new parameter value is written to the product configuration table. The change is logged to MOD-047 with full before/after state.

Audit trail

Every proposal — whether approved, rejected, or superseded — is retained permanently. The table is append-only for audit purposes; no rows are deleted. MOD-047 receives a log entry for every status transition. The combination provides a complete, queryable history of who proposed what, who approved it, when the notification was sent, and when the change took effect.

Requirements

FR-573 — Maker/checker enforcement: every product configuration change proposal must require approval by a second authorised staff member who is distinct from the proposer; the system must enforce this at the database constraint level; any attempt to self-approve must be rejected with a clear error.

FR-574 — Disclosure gate: any proposal that constitutes an unfavourable change to customers must not apply before the required notification period has elapsed after MOD-063 confirms dispatch; the system must block application of the change at the effective date gate if notification_confirmed_at IS NULL.

FR-575 — Configuration propagation: applied configuration changes must be propagated to all dependent modules — including MOD-110 (fee engine), MOD-050 (disclosure management), and MOD-113 (statement generation) — within 60 seconds of the change being applied, so that live product behaviour and customer disclosures are consistent with the new configuration.

FR-576 — Immutable audit log: every proposal and every status transition must be logged to MOD-047 with the staff member ID, timestamp, and full before/after parameter values; the log must be queryable by product, parameter, and date range; no records may be deleted or modified after creation.

Policies satisfied:

Policy Mode Description
GOV-006 — Internal Audit Policy LOG Every product configuration change — rate, fee, term, threshold — is logged immutably with the proposer, approver, timestamp, and before/after values for full audit trail coverage.
CON-005 — Fee & Pricing Transparency Policy GATE Any rate or fee change that is unfavourable to customers is blocked from taking effect until MOD-063 confirms that all affected customers have been notified with the required advance notice period.
REP-002 — Prudential Reporting Policy AUTO Rate and fee changes are automatically published to the product data feed consumed by MOD-113 statement generation and MOD-050 disclosure management, ensuring disclosures remain consistent with live configuration.
GOV-007 — Conflicts of Interest Policy GATE Every configuration change requires a four-eyes check — the proposer cannot be the same person as the approver; the system enforces this at the database constraint level, not only in application logic.

MOD-129 — Teller operations and branch cash management

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Provides a teller workstation mode for branch staff to process over-the-counter cash transactions on behalf of customers. Covers cash deposits, cash withdrawals, and branch cash drawer management. Designed for community banks and building societies that operate physical branches as a service channel alongside the digital app.

Scope

This module is limited to domestically-denominated cash transactions. Not in scope: foreign currency exchange, traveller's cheques, or money order issuance. (All NZ Big 4 banks have exited international cash services; this is not a standard teller function at any NZ bank and is not a reasonable expectation for a building society or small community bank deploying this platform.)

Teller role context

The module introduces a dedicated teller JWT role. Staff with this role access the teller workstation — a focused back-office UI separate from the standard back-office panel — which presents only the controls relevant to in-branch transactions. The teller's session is bound to a branch code and cash drawer ID. All postings made during a teller session carry the teller's staff_id, the branch_code, and the drawer_id in the posting metadata.

Tellers can: - Search for customer accounts by customer ID, account number, or verified identity document. - Post cash deposits and cash withdrawals to customer accounts. - Trigger a KYC identity check via MOD-009 for customers without a card, or for transactions requiring enhanced identity verification. - Open and close their cash drawer session with opening/closing balance reconciliation. - Record a cash drawer variance with a reason note.

Tellers cannot: - Override pre-payment validation (balance, sanctions, account status). - Modify customer records. - Access credit decision functions. - Process transactions outside their authorised cash limit without supervisor override (configurable per branch).

AML reporting

Cash transactions above the prescribed reporting threshold trigger the AML cash transaction reporting workflow. Thresholds are jurisdiction-specific:

  • NZ: NZD 10,000 or equivalent — required reporting to the Police Financial Intelligence Unit under the AML/CFT Act 2009, s.48.
  • AU: AUD 10,000 or equivalent — required Threshold Transaction Report (TTR) to AUSTRAC under the AML/CTF Act 2006, s.43.

The threshold check runs automatically at the point of teller confirmation. When triggered: the teller is prompted to confirm the customer's identity (MOD-009 verification or document inspection), the transaction is flagged as ctr_required, and the cash transaction report is queued for submission via the AML monitoring module. The posting is not finalised until the identity confirmation is recorded.

Structuring detection — multiple transactions below the threshold that together appear designed to avoid reporting — is handled by the AML monitoring platform (MOD-034, SD03), not by this module. MOD-129 provides the raw transaction data; MOD-034 identifies structuring patterns.

Data model

-- app.teller_sessions
CREATE TABLE app.teller_sessions (
  session_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  staff_id         UUID NOT NULL,
  branch_code      TEXT NOT NULL,
  drawer_id        TEXT NOT NULL,
  opened_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  opening_balance  NUMERIC(18,2) NOT NULL,
  closed_at        TIMESTAMPTZ,
  closing_balance  NUMERIC(18,2),
  expected_balance NUMERIC(18,2),  -- calculated from transactions
  variance         NUMERIC(18,2),  -- closing_balance - expected_balance
  variance_reason  TEXT,
  status           TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed','variance_noted'))
);

-- app.teller_transactions
CREATE TABLE app.teller_transactions (
  teller_tx_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  session_id       UUID NOT NULL REFERENCES app.teller_sessions(session_id),
  account_id       UUID NOT NULL,
  transaction_type TEXT NOT NULL CHECK (transaction_type IN ('cash_deposit','cash_withdrawal')),
  amount           NUMERIC(18,2) NOT NULL,
  currency         TEXT NOT NULL DEFAULT 'NZD',
  ctr_required     BOOLEAN NOT NULL DEFAULT false,
  identity_method  TEXT CHECK (identity_method IN ('card','document_inspection','eidv')),
  posting_id       UUID,  -- MOD-001 posting reference
  staff_id         UUID NOT NULL,
  branch_code      TEXT NOT NULL,
  created_at       TIMESTAMPTZ NOT NULL DEFAULT now()
);

Key operations

Cash deposit

The teller searches for the customer's account, enters the cash amount, confirms the notes and coins tendered, and submits. Pre-payment validation runs (account status, sanctions screen). On validation pass: the deposit is posted via MOD-001 as a debit to the branch cash account and a credit to the customer's account. The customer's available balance is updated immediately. The teller transaction record is created and linked to the MOD-001 posting. The teller's session expected balance increases by the deposit amount.

If the amount meets or exceeds the AML reporting threshold, the identity confirmation step is inserted before the posting is finalised.

Cash withdrawal

The teller enters the withdrawal amount. MOD-003 is called to confirm available balance. If sufficient: pre-payment validation runs. On pass: the withdrawal is posted via MOD-001. The customer's available balance is reduced. The teller's session expected balance decreases.

If the amount meets or exceeds the AML reporting threshold, the identity confirmation step runs before the withdrawal can be authorised.

Supervisor override

Cash withdrawals above a configurable per-session limit require supervisor authorisation. The teller submits a supervisor override request; a back-office supervisor approves from their own authenticated session. The override approval is recorded alongside the teller transaction. This control is configurable per branch, allowing institutions with higher-trust environments to raise the threshold.

Session close

At end of day (or end of shift), the teller counts their drawer and enters the physical closing balance. The system calculates the expected balance from all transactions in the session. If variance > configured_tolerance, status = variance_noted and the teller must record a variance reason. The session record, all transactions, and any variance note are passed to MOD-081 for branch reconciliation.

Requirements

FR-581 — Teller identity binding: every teller posting must carry the teller's staff_id, branch_code, and drawer_id in the posting metadata recorded in MOD-001; no teller posting may be made without an active teller session; the teller identity must be immutable on the posting record after confirmation.

FR-582 — AML threshold gate: any cash transaction at or above the jurisdiction-specific reporting threshold must trigger the identity confirmation step before the posting is finalised; the ctr_required flag must be set on the teller transaction record; the posting must not proceed until a valid identity method is recorded.

FR-583 — Pre-payment validation: all teller-initiated cash withdrawals must pass the same pre-payment validation checks (available balance via MOD-003, account status, sanctions screen) as digital channel payments; no teller bypass path may exist that authorises a withdrawal when validation would return a decline for the same transaction from the digital channel.

FR-584 — Session reconciliation: on session close, the system must calculate expected_balance from the sum of all transactions in the session, compare it to the teller-entered closing_balance, and record any variance; sessions with a variance outside the configured tolerance must be flagged to the branch operations queue and must not be closed without a variance reason being recorded.

Policies satisfied:

Policy Mode Description
AML-005 — Transaction Monitoring Policy GATE Cash transactions above the prescribed reporting threshold require teller-initiated identity verification and are automatically submitted to the cash transaction reporting workflow before the posting is finalised.
PAY-001 — Payment Operations Policy GATE All teller-initiated postings pass the same pre-payment validation (available balance, account status, sanctions screen) as digital channel payments — no teller bypass path exists.
GOV-006 — Internal Audit Policy LOG Every teller transaction is logged with the teller's staff ID, branch code, session ID, and timestamp in addition to the standard transaction audit trail — the teller identity is immutably recorded at the database layer.
CON-001 — Customer Fairness & Conduct Policy AUTO Cash receipts are posted and the customer's available balance is updated immediately upon teller confirmation — the customer's balance is never temporarily reduced or withheld pending a manual reconciliation step.

MOD-131 — Mutual governance and AGM administration

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Provides the governance workflow tooling required for mutual institutions — building societies and credit unions — to discharge their statutory AGM obligations, conduct member voting, and distribute their annual report. Operates exclusively when the platform is deployed with institution_type: mutual.

This module is a companion to MOD-118 (member equity and share registry), which manages the member register and share capital. MOD-131 manages the periodic governance events (AGM, extraordinary general meetings, board elections) that mutual institutions must conduct under their enabling legislation.

Enabling legislation

New Zealand. Building societies are incorporated under the Building Societies Act 1965 and must hold an annual general meeting. Member-elected boards are a statutory requirement; board election results must be filed with the Registrar of Companies. Credit unions operate under the Friendly Societies and Credit Unions Act 1982 with similar AGM obligations. Under the Depositor Compensation Scheme established by the Deposit Takers Act 2023, the member register maintained in MOD-118 must be accurate as at each reporting date.

Australia. Mutual ADIs (building societies and credit unions) are incorporated under state legislation or as companies limited by guarantee, and must hold an AGM under the Corporations Act 2001. APRA-regulated mutual ADIs are also subject to the Mutual Equity Interest (MEI) provisions and the Basel III Common Equity Tier 1 criteria for cooperative/mutual institutions. Member voting rights and their relationship to capital instruments must be documented in the institution's constitution.

AGM workflow

The AGM workflow covers five stages:

1. Notice generation. The back-office governance team initiates an AGM in the module, specifying the meeting date, venue or virtual meeting platform, agenda items, and notice period. The system queries MOD-118 to identify all eligible members (active status, shareholding ≥ 1 share as at the record date). The AGM notice document and proxy form are uploaded to MOD-073 and dispatched to all eligible members via MOD-063 on the notice date. The notice period is enforced as a gate — the meeting cannot proceed unless notice was dispatched at least the statutory minimum days before the meeting (configurable; NZ Building Societies Act: 14 days minimum; Corporations Act AU: 28 days minimum for public companies, 21 days for proprietary equivalent).

2. Proxy submission. Members who cannot attend in person may lodge a proxy. Proxies may be submitted digitally via the member portal or in writing. The module records each proxy with the member ID, nominated proxy holder, any directed voting instructions, and receipt timestamp. Proxy submissions after the cut-off time (typically 48 hours before the meeting) are rejected.

3. Quorum check. On meeting day, the back-office governance operator initiates the quorum calculation. The system counts members present or represented by proxy, expressed as a percentage of total eligible members. If quorum is not reached, the meeting must be adjourned or the threshold recalculated per the institution's constitution. Quorum rules are configurable.

4. Voting and result recording. For each resolution (ordinary, special, or board election), votes are recorded by the back-office operator. Results are calculated as: votes in favour, votes against, abstentions, and the resolution outcome (passed or failed). For board elections, a ranked count is run against the candidate list and the elected directors are recorded with their term start date.

5. Minutes and result filing. The AGM minutes are uploaded to MOD-073 and distributed to all members via MOD-063 within the statutory period after the meeting. Board election results are flagged for filing with the relevant registrar (Companies Office NZ, ASIC AU) — the module produces a structured filing summary but does not connect directly to registrar APIs (filing remains a manual step).

Data model

-- app.general_meetings
CREATE TABLE app.general_meetings (
  meeting_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  meeting_type      TEXT NOT NULL CHECK (meeting_type IN ('agm','egm')),
  meeting_date      DATE NOT NULL,
  notice_date       DATE NOT NULL,
  notice_period_days INT NOT NULL,
  record_date       DATE NOT NULL,  -- member eligibility determined at this date
  eligible_members  INT,            -- populated when notice is dispatched
  quorum_threshold_pct NUMERIC(5,2) NOT NULL,
  quorum_met        BOOLEAN,
  status            TEXT NOT NULL DEFAULT 'planned'
                    CHECK (status IN ('planned','notice_dispatched','in_progress','completed','adjourned')),
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- app.meeting_resolutions
CREATE TABLE app.meeting_resolutions (
  resolution_id     UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  meeting_id        UUID NOT NULL REFERENCES app.general_meetings(meeting_id),
  resolution_number INT NOT NULL,
  resolution_type   TEXT NOT NULL CHECK (resolution_type IN ('ordinary','special','board_election')),
  description       TEXT NOT NULL,
  votes_for         INT,
  votes_against     INT,
  abstentions       INT,
  proxy_votes_for   INT,
  proxy_votes_against INT,
  outcome           TEXT CHECK (outcome IN ('passed','failed','deferred')),
  resolved_at       TIMESTAMPTZ
);

-- app.meeting_proxies
CREATE TABLE app.meeting_proxies (
  proxy_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  meeting_id        UUID NOT NULL REFERENCES app.general_meetings(meeting_id),
  member_id         UUID NOT NULL,  -- MOD-118 member_id
  proxy_holder_id   UUID,           -- member_id of proxy holder, if another member
  directed_votes    JSONB,          -- {resolution_id: 'for'|'against'|'abstain'}
  received_at       TIMESTAMPTZ NOT NULL,
  valid             BOOLEAN NOT NULL DEFAULT true,
  invalidation_reason TEXT
);

Annual report distribution

The annual report workflow is separate from the AGM workflow but typically runs on the same timeline. The governance team uploads the finalised annual report PDF to MOD-073. The module dispatches the annual report to all active members via MOD-063 (push notification with in-app and email delivery). Delivery status is tracked per member — members who did not receive delivery are queued for postal dispatch (external to the platform) if applicable.

Annual report dispatch is recorded as a governance event and logged to MOD-047. Regulators (RBNZ, APRA) may request evidence that the annual report was made available to all members; the dispatch record provides that evidence.

Configuration flag

institution_type: mutual   # enables MOD-131 and MOD-118

When institution_type: proprietary, this module is inactive. All GOV policy workflows and back-office governance tools continue to function, but the mutual-specific AGM and member voting functions are hidden. This prevents a proprietary bank deploying this platform from inadvertently accessing mutual governance tooling.

Requirements

FR-585 — Notice period gate: the AGM workflow must enforce the configured minimum notice period as a technical gate; the meeting status must not advance past notice_dispatched unless MOD-063 confirms that the notice was dispatched to all eligible members at least the minimum number of days before the meeting date; the gate must be enforced based on the dispatch confirmation timestamp, not the scheduled dispatch date.

FR-586 — Proxy management: the module must record every proxy submission with the member ID, receipt timestamp, and any directed voting instructions; proxies received after the configured cut-off must be automatically rejected with a rejection record; the total proxy count and directed vote breakdown must be included in the quorum and voting calculations.

FR-587 — Voting record integrity: resolution outcomes must be calculated from the recorded votes and must not be manually entered as a final result without the supporting vote count; the resolution record must include votes_for, votes_against, abstentions, and the outcome derived from those figures; the AGM minutes document must be stored in MOD-073 and dispatched to all members within 30 days of the meeting date.

FR-588 — Annual report distribution: the annual report dispatch must be triggered from within the governance workflow, dispatched via MOD-063 to all active members, and the delivery status tracked per member; the dispatch event must be logged to MOD-047 with the document reference from MOD-073 and a count of successful and failed deliveries.

Policies satisfied:

Policy Mode Description
GOV-001 — Board Charter AUTO AGM notice, agenda, and proxy voting processes are delivered through a governed workflow that ensures all members on the register receive notice within the statutory timeframe and that quorum calculations are based on the live member register from MOD-118.
CON-001 — Customer Fairness & Conduct Policy AUTO All eligible members receive AGM notice, proxy forms, and annual report simultaneously via their preferred channel — no member receives less notice than any other.
REP-002 — Prudential Reporting Policy LOG AGM outcomes, board election results, and proxy voting tallies are recorded as immutable governance events and are available for regulatory examination and member inspection on request.
GOV-006 — Internal Audit Policy LOG All AGM administration actions — notice dispatch, proxy receipt, vote recording, result declaration — are logged with the acting staff member's ID and timestamp.

MOD-138 — Deceased customer and estate management

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Manages the end-to-end lifecycle of a deceased customer's accounts, from notification of death through account freeze, legal personal representative (LPR) registration, estate access, distribution of balances, and final account closure. This module exists to ensure that bereaved families and estates are treated with appropriate care, that the bank meets its legal obligations under estate administration law, and that account access is granted only where legally authorised — protecting both the estate and the bank from fraud.

Errors in deceased estate handling are a consistent source of financial ombudsman complaints in both NZ and AU. The regulatory focus from the FMA and ASIC on vulnerable customer obligations extends explicitly to how banks treat deceased estates and the people dealing with them.

Executor vs administrator

An executor is a person named in the deceased's will to administer the estate. An administrator is appointed by the court where there is no will (intestate), or where the named executor is unable or unwilling to act. Both are legal personal representatives (LPRs) with equivalent authority to administer the estate.

The LPR's authority derives from:

  • Grant of probate — a court order confirming the validity of the will and authorising the executor to act. Issued by the High Court (NZ) or the relevant state Supreme Court (AU).
  • Letters of administration — a court order appointing an administrator where there is no valid will or no executor able to act.

Until probate or letters of administration are granted, no person has legal authority to deal with the deceased's estate other than to safeguard it. The bank cannot release funds to family members, co-account holders (except for joint accounts with survivorship), or even a named executor prior to production of the relevant court order — unless the small estate concession applies.

Small estate concessions

Both NZ and AU have simplified processes for small estates where obtaining full probate would be disproportionately costly and burdensome.

  • New Zealand: A bank may release estate funds without probate where the total estate held at that bank is below approximately NZD 15,000 (the exact threshold is set by individual banks in compliance with general practice; there is no statutory maximum). The LPR must provide a statutory declaration or affidavit of claim confirming their relationship to the deceased, that they are entitled to the funds, and that no other person has a competing claim.
  • Australia: Similar small estate provisions apply, generally at approximately AUD 15,000 per financial institution. State succession legislation (e.g., Succession Act 2006 (NSW), Administration and Probate Act 1958 (Vic)) sets the framework but the practical threshold is determined by each bank's policy.

The small estate threshold is configurable per jurisdiction in this module. The bank's back-office team retains discretion to require full probate even for estates below the threshold if there are competing claims or suspicious circumstances.

Joint accounts

Joint accounts with a right of survivorship (the standard structure in NZ and AU) pass automatically to the surviving account holder on death — the surviving holder gains sole ownership by operation of law. The bank does not require probate for joint account funds to pass to the survivor. However, the account must still be administratively updated to remove the deceased customer as a holder, and death notification must be recorded.

Accounts held as tenants in common (less common in retail banking) require the deceased's share to pass through the estate.

Account freeze

On receipt of a notification of death, confirmed by a back-office staff member, the following actions are taken immediately (within 60 seconds):

  1. All outgoing payments from the deceased customer's accounts are suspended — direct debits, standing orders, scheduled payments, and any pending payment instructions are cancelled or placed on hold.
  2. Any existing PoA or EPoA on the deceased's accounts is set to revoked_by_death via MOD-126. PoA ceases by operation of law on the death of the grantor; this action makes the legal position explicit in the system.
  3. The account state is updated to deceased_estate via MOD-007.
  4. Incoming credits continue to be received. Employers, government agencies, or other payers may continue to send credits to the account — these are held for the estate and form part of the distributable balance.

Outgoing credit card transactions on a linked card are also blocked. If the deceased held an overdraft or credit facility, the facility is placed into maintenance mode — interest continues to accrue against the estate liability but no further draws are permitted.

The freeze is recorded in the estate log with the notifying staff member's identity and timestamp.

LPR registration workflow

Before any person can be granted access to a deceased customer's accounts, they must be registered as the LPR for that estate. The registration workflow:

  1. Identity verification — the proposed LPR must pass eIDV via MOD-009. If the LPR is already a customer with a verified KYC record, their existing record is used. If they are not an existing customer, they complete eIDV as a non-account-holder.

  2. Authority document upload — the LPR uploads the relevant authority document to MOD-073:

  3. Grant of probate (executor)
  4. Letters of administration (administrator)
  5. Small estate affidavit or statutory declaration (small estate pathway)

  6. Back-office review — a back-office staff member reviews both the eIDV result and the uploaded document. The review requires:

  7. Confirming the grant/letters are addressed to the person claiming to be the LPR.
  8. Confirming the document references the correct deceased person (name, date of death).
  9. Confirming the document is not expired or superseded.
  10. A two-step approval: the reviewing agent approves, and a supervisor countersigns before access is activated.

  11. Access activation — once both approvals are complete, the LPR is granted estate_access scope on the deceased's accounts. This scope allows the LPR to view balances and transaction history, initiate distributions, and request account closure. It does not allow the LPR to originate new financial commitments (new direct debits, loans, etc.).

All steps are logged immutably via MOD-047 and in the estate event log.

Small estate process

Where the total balance across all the deceased's accounts at this bank is below the configured small estate threshold for the relevant jurisdiction, the simplified pathway applies:

  1. The LPR (typically a family member) still completes eIDV via MOD-009.
  2. In lieu of probate, the LPR submits a statutory declaration or affidavit of claim confirming:
  3. Their identity and relationship to the deceased.
  4. That they are entitled to the funds (as sole beneficiary, executor under an unproved will, or nearest surviving relative in the absence of a will).
  5. That no other person has a competing claim to the funds.
  6. Back-office approval is required — a single approver (no supervisor countersign required for small estates below threshold).
  7. Distribution to the declared beneficiary is processed once the affidavit is approved.

The system checks the total balance across all accounts and compares it against the jurisdiction threshold at the point of LPR registration. If the balance exceeds the threshold, the simplified pathway is blocked and the system directs the LPR to obtain probate or letters of administration.

Data model

-- app.deceased_estates
CREATE TABLE app.deceased_estates (
  estate_id                   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id                 UUID NOT NULL,
  notification_date           DATE NOT NULL,
  notification_source         TEXT NOT NULL CHECK (notification_source IN (
                                'customer_family','solicitor','public_trustee','court')),
  death_certificate_id        UUID,  -- MOD-073 document reference
  estate_type                 TEXT CHECK (estate_type IN (
                                'full_probate','letters_of_administration','small_estate')),
  probate_reference           TEXT,
  lpr_customer_id             UUID,
  lpr_authority_document_id   UUID,  -- MOD-073 document reference
  estate_threshold_applies    BOOLEAN NOT NULL DEFAULT false,
  status                      TEXT NOT NULL DEFAULT 'notified' CHECK (status IN (
                                'notified','frozen','lpr_registered','active',
                                'distributing','closed')),
  notes                       TEXT,
  created_at                  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- app.estate_distributions
CREATE TABLE app.estate_distributions (
  distribution_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  estate_id          UUID NOT NULL REFERENCES app.deceased_estates(estate_id),
  account_id         UUID NOT NULL,
  amount             NUMERIC(18,2) NOT NULL,
  distribution_type  TEXT NOT NULL CHECK (distribution_type IN ('full_balance','partial')),
  recipient_account  TEXT NOT NULL,
  posting_id         UUID,  -- reference to the transaction posted via MOD-001
  distributed_at     TIMESTAMPTZ
);

notification_source records how the bank was informed — this is important for regulatory purposes: deaths notified by a solicitor or court typically come with documentation already in hand; deaths notified by family members require more active document collection.

estate_threshold_applies is set to true at registration time if the total balance at notification was below the jurisdiction threshold. It is not recalculated once set — if balances change during the estate period (e.g., an employer pays a final salary), the determination stands. Back-office staff can manually override this flag with a documented reason.

Key operations

Notification and freeze

Death notification is received via the back-office channel (branch staff, phone banking staff, or digital back-office) or via written notification from a solicitor or the Public Trustee. Self-notification via the customer app is not supported for death notifications.

The back-office agent confirms the notification, creates the deceased_estates record with status = notified, and the freeze actions described above execute automatically.

PoA revocation

On creation of the deceased_estates record, the system queries MOD-126 for any active PoA, EPoA, or third-party authority on the deceased customer's accounts and sets each to revoked_by_death in the same database transaction as the freeze. This ensures the two actions are atomic — it is not possible for a PoA to remain active on a frozen deceased account.

LPR registration

The LPR registration workflow is initiated by the LPR contacting the bank (by phone, visiting a branch, or via written correspondence). The bank provides the LPR with a checklist of required documents and the upload pathway (via the back-office portal or by post/in-branch scanning). Registration is completed by back-office staff once all documents are received and the two-step approval is complete.

Estate access period

Once the LPR is registered and status = active, the LPR can:

  • View all account balances and the full transaction history.
  • Instruct distributions to nominated beneficiaries or recipient accounts.
  • Provide instructions to close credit facilities (with the outstanding balance forming an estate liability).
  • Request statements for all periods of the deceased's account history.

The LPR cannot open new accounts, change account terms, or grant access to any other person.

Distribution and closure

Distributions are initiated by the LPR and approved by a back-office agent. Each distribution is posted via MOD-001 with the estate_id and distribution_id recorded on the transaction. For full balance distributions, the account balance must be zero after the distribution posts.

Once all accounts have been distributed to zero, the back-office agent updates the account status to deceased_closed via MOD-007 and updates the deceased_estates record to status = closed. The customer record is updated to reflect deceased status — the customer is not deleted from the system, as their records must be retained for the minimum regulatory retention period.

Jurisdictional note

Both NZ and AU use court-issued authority documents (probate / letters of administration) as the legal mechanism for recognising an LPR. The processes are broadly equivalent but administered by different courts:

  • NZ: High Court grants probate and letters of administration. The New Zealand Public Trust can act as administrator where no family member applies. The Administration Act 1969 and Administration (Probate) Rules 1968 govern the process.
  • AU: Each state's Supreme Court (or the relevant Probate Registry) grants probate and letters of administration. Interstate grants require re-sealing in other states (though banks typically accept original state grants for accounts held in any state). The bank must accept interstate grants without requiring re-sealing given the bank's national operations.

Both jurisdictions' small estate concessions allow the bank to release funds below the threshold on statutory declaration — this avoids the expense and delay of a full court application for small estates.

Requirements

FR-613 — Freeze on notification: upon recording a notification of death, the system shall immediately suspend all outgoing payments from the deceased customer's accounts (direct debits, standing orders, scheduled payments) within 60 seconds of the notification being confirmed by a back-office staff member, while allowing incoming credits to continue to be received; any existing PoA or EPoA on the deceased's accounts must be automatically set to status revoked_by_death via MOD-126 in the same operation.

FR-614 — LPR registration gate: the system shall require verification of the LPR's identity via MOD-009 and upload of the LPR's legal authority document to MOD-073 (grant of probate, letters of administration, or small estate affidavit) before granting the LPR any access to the deceased customer's accounts; the back-office staff member must complete a two-step approval of both the identity check and the legal authority document before access is activated.

FR-615 — Small estate pathway: the system shall support the small estate simplified pathway; for estates where the total balance across all accounts is below the configured small estate threshold, the system must accept a statutory declaration or affidavit of claim in lieu of probate, with back-office approval, and must allow distribution to the declared beneficiary without requiring full probate — the threshold must be configurable per jurisdiction.

FR-616 — Distribution and closure: the system shall process estate distribution by posting the distribution amount from the deceased's account to the nominated recipient account or external account via MOD-001, recording the estate_id and distribution_id on the transaction, and must close the deceased customer's account and update the customer record to deceased_closed status upon confirmation that all accounts have been distributed and balances are zero.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy GATE Account access for a legal personal representative (LPR) requires verification of their identity and their legal authority (probate/letters of administration) before any access is granted; no access is provided based on claimed authority alone.
GOV-006 — Internal Audit Policy LOG All deceased estate events (notification of death, freeze, LPR registration, access grants, distributions, closure) are logged immutably with the acting staff member and timestamp.
CON-003 — Vulnerable Customer Policy AUTO The account holder's deceased status triggers enhanced care obligations — all estate communications are managed with empathy protocols and automated payment outflows are suspended on notification.
AML-002 — Customer Due Diligence (CDD) Policy LOG The LPR must pass KYC before being granted account access; the LPR's identity and authority are recorded and available for AML audit.

MOD-139 — Financial hardship formal variation workflow

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Implements the statutory hardship variation process that lenders in NZ and AU are required to offer to customers experiencing genuine financial difficulty. When a customer cannot meet their loan repayments due to circumstances beyond their reasonable control, they have a legal right to apply for hardship assistance, and the bank has a legal obligation to assess that application within a defined timeframe and, where the customer qualifies, agree to a formal variation of the loan contract.

This module is distinct from informal collections arrangements (MOD-065) in a critical way: a hardship variation is a legally binding contract variation, documented, disclosed to the customer in writing, and carrying regulatory enforceability. It is not a discretionary forbearance arrangement — it is a statutory entitlement. The failure to run a compliant hardship process is a category of harm that both the FMA (NZ) and ASIC (AU) actively investigate and that generates significant enforcement action and public findings.

Regulatory framework

NZ — CCCFA section 102

The Credit Contracts and Consumer Finance Act 2003 (CCCFA) section 102 gives borrowers under consumer credit contracts the right to apply for a hardship variation. The lender must consider the application and respond within 10 working days of receiving it. If the lender agrees the borrower is facing genuine financial difficulty and is reasonably likely to be able to meet the varied terms, it must agree to the variation.

The lender may decline only on specific grounds — primarily that the borrower is not in genuine financial difficulty, or that the proposed variation would not result in a sustainable payment plan. The lender cannot decline solely because a variation would reduce the bank's expected return or increase the bank's credit risk exposure.

If the lender fails to respond within 10 working days, or declines without proper grounds, the borrower may apply to the court for a variation order. The Commerce Commission also monitors compliance with CCCFA s102.

AU — NCCP Part 4.1A

The National Consumer Credit Protection Act 2009 (NCCP) Part 4.1A (inserted by the National Consumer Credit Protection Amendment (Enhancements) Act 2012) provides an equivalent right for Australian borrowers under regulated credit contracts. Key differences from NZ:

  • The assessment deadline is 21 calendar days from receipt of the application (not working days).
  • ASIC Regulatory Guide 203 provides detailed guidance on responsible conduct of the hardship process.
  • The bank must notify the customer in writing of its decision and, if declining, give the reasons.
  • Credit default listings and court proceedings may not be initiated while a hardship application is under assessment.

Both regimes are underpinned by the same principle: a lender that offers consumer credit takes on a responsibility to assist customers through temporary financial difficulty rather than immediately escalating to collections or default proceedings.

Variation types

Variation type Description Effect on contract
Payment holiday Full pause on all repayments for a defined period Interest continues to accrue; term extends by the pause period; total interest cost increases
Reduced repayments Minimum repayment amount reduced for a defined period Term extends; total interest cost increases
Interest capitalisation No repayments for a period; interest is capitalised (added to principal) Principal balance increases; subsequent repayments recalculated on higher balance
Term extension Same repayment amount, term extended to reduce monthly obligation Total interest cost increases; monthly burden decreases
Partial capitalisation Interest-only payments for a period; principal repayments deferred Principal unchanged during period; total interest cost increases

All variation types increase the total cost of credit to the customer compared with the original contract. The variation agreement must make this explicit — the customer must be shown the revised total interest payable and total amount repayable before they accept the variation. This disclosure is produced and delivered via MOD-050.

Assessment criteria

The bank must assess three things:

  1. Genuine financial difficulty — the customer is currently unable or likely to be unable to meet their repayments due to illness, injury, loss of employment, the end of a relationship, a natural disaster, or any other reasonable cause.
  2. Reasonable likelihood of recovery — the customer is reasonably likely to be able to meet the varied repayment terms. A customer with no foreseeable income and no prospects of recovery may not meet this test — but the bar is low and benefit of the doubt is given.
  3. Ability to meet varied terms — the proposed variation creates terms the customer can realistically afford.

The bank cannot refuse a hardship application solely because:

  • The variation would reduce the bank's expected return.
  • The customer has previously been in hardship.
  • The customer's overall financial position is poor.
  • The loan is already in arrears.

Assessors are provided with assessment guidance in MOD-053. All decisions are documented with reasons.

Data model

-- app.hardship_applications
CREATE TABLE app.hardship_applications (
  application_id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  account_id                  UUID NOT NULL,
  customer_id                 UUID NOT NULL,
  application_channel         TEXT NOT NULL CHECK (application_channel IN (
                                'app','branch','phone','written')),
  reason_category             TEXT NOT NULL CHECK (reason_category IN (
                                'job_loss','illness','relationship_breakdown',
                                'natural_disaster','other')),
  reason_detail               TEXT,
  variation_requested         TEXT NOT NULL,
  application_date            DATE NOT NULL,
  assessment_due_date         DATE NOT NULL,
  status                      TEXT NOT NULL DEFAULT 'received' CHECK (status IN (
                                'received','under_assessment','variation_offered',
                                'accepted','declined','withdrawn')),
  decision_date               DATE,
  assessor_id                 UUID,
  decision_notes              TEXT,
  variation_agreement_document_id UUID,  -- MOD-073 document reference
  created_at                  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- app.hardship_variations
CREATE TABLE app.hardship_variations (
  variation_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  application_id        UUID NOT NULL REFERENCES app.hardship_applications(application_id),
  account_id            UUID NOT NULL,
  variation_type        TEXT NOT NULL CHECK (variation_type IN (
                          'payment_holiday','reduced_repayments','interest_capitalisation',
                          'term_extension','partial_capitalisation')),
  original_repayment    NUMERIC(18,2) NOT NULL,
  varied_repayment      NUMERIC(18,2) NOT NULL,
  variation_start_date  DATE NOT NULL,
  variation_end_date    DATE NOT NULL,
  capitalised_amount    NUMERIC(18,2),
  status                TEXT NOT NULL DEFAULT 'active' CHECK (status IN (
                          'active','completed','defaulted')),
  confirmed_at          TIMESTAMPTZ,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);

assessment_due_date is calculated at the point of application creation: 10 working days from application_date for NZ accounts, 21 calendar days for AU accounts. The jurisdiction is determined from the account's product configuration. This field is immutable after creation — it records the statutory deadline, not an internal target.

capitalised_amount is populated for interest_capitalisation and partial_capitalisation variation types only. It records the total interest amount that was added to the principal balance on commencement of the variation.

Key operations

Application submission

A customer submits a hardship application via any channel — app self-service, calling the phone banking team, visiting a branch, or sending written correspondence. The application must record the channel (for audit) and the reason category.

On submission, the system:

  1. Creates the hardship_applications record with status = received.
  2. Sets assessment_due_date based on the jurisdiction of the account.
  3. Creates a case in MOD-053 with case_type: hardship_application, linking the application_id.
  4. Emits a bank.app.hardship_application_received event.

Collections suspension

If the account has any active collections activity in MOD-065 (e.g., a late payment workflow, outbound contact schedule, or default notice in preparation), the hardship application suspends all collections activity immediately. The suspension is maintained until either:

  • The application is declined and the suspension period has elapsed, or
  • A variation agreement is confirmed and active.

No default notice, credit default listing, or legal proceedings may be initiated while a hardship application is under assessment. This is a legal prohibition in both NZ (CCCFA) and AU (NCCP), not a discretionary policy.

Assessment workflow in MOD-053

The hardship application case in MOD-053 follows a structured assessment workflow:

  • The assessor reviews the customer's account history, income, and stated reason for hardship.
  • The assessor records their assessment of the three criteria (genuine difficulty, likelihood of recovery, ability to meet varied terms).
  • If granting: the assessor selects the variation type and parameters, and the system generates a variation offer.
  • If declining: the assessor records the specific grounds for the decline. The customer is notified in writing with the reasons and information about their right to seek an independent review.

Alert thresholds via MOD-063:

  • 5 working days before assessment_due_date: alert to assessor and supervisor.
  • On assessment_due_date if status is still received or under_assessment: escalation alert to supervisor.
  • If deadline is missed: immediate escalation to the head of the collections and hardship team; the case is flagged as a potential regulatory breach and logged accordingly.

Variation offer and customer acceptance

The variation offer is presented to the customer with a disclosure document produced by MOD-050. The disclosure shows:

  • The current loan terms.
  • The proposed varied terms, including the repayment amount during the variation period.
  • The variation end date.
  • The revised total interest payable and total amount repayable over the full loan term.
  • The effect of any capitalised interest on the principal balance.

The customer must explicitly accept the variation offer — acceptance by silence or inaction is not permitted. Acceptance is recorded with a timestamp in the hardship_applications record.

Schedule regeneration and variation activation

On customer acceptance, the following steps execute atomically:

  1. MOD-112 recalculates the amortisation schedule under the varied terms.
  2. MOD-050 delivers the formal variation agreement (including the revised schedule) to the customer.
  3. MOD-007 sets the hardship flag on the account and transitions the account state to hardship_variation.
  4. For capitalisation variation types: MOD-001 posts the capitalised interest amount to the principal balance.
  5. The hardship_variations record is set to status = active and confirmed_at is recorded.

All five steps must complete before the variation is considered active. If any step fails, the entire operation is rolled back.

Variation monitoring

A daily monitoring job runs for all accounts with status = active hardship variations:

  • Missed repayment detection: if a varied repayment is missed during the variation period, the back-office hardship team is alerted via MOD-063. A missed varied repayment is treated differently from a standard collections trigger — the hardship team contacts the customer to understand whether further assistance is needed.
  • End-date proximity: 14 days before variation_end_date, the customer and the back-office team are notified. This allows time to either prepare for reversion to original terms or assess whether a further variation is needed.
  • End-date reversion: on variation_end_date, the variation is set to status = completed and the account reverts to the original repayment terms as shown in the revised amortisation schedule generated at activation. MOD-007 clears the hardship flag. MOD-063 notifies the customer and the hardship team that the variation has ended.

Requirements

FR-617 — Statutory deadline tracking: the system shall record the statutory assessment deadline on every hardship application at the point of submission — 10 working days from receipt for NZ (CCCFA s102), 21 calendar days for AU (NCCP Part 4.1A) — and must alert the assessor via MOD-063 at 5 days before the deadline and again at the deadline if no decision has been recorded; the application status must be escalated to a supervisor if the deadline is missed.

FR-618 — Collections suspension: the system shall suspend all collections activity from MOD-065 for an account once a hardship application is received, maintaining the suspension until either the application is declined or a variation agreement is confirmed and active; no new collections escalation, default notice, or credit default listing may be initiated during the hardship assessment period.

FR-619 — Atomic variation activation: the system shall, upon a hardship variation being accepted by the customer, regenerate the loan's amortisation schedule via MOD-112 reflecting the varied terms, deliver the updated schedule to the customer via MOD-050 as the formal variation agreement, set the account's hardship flag in MOD-007, and post any capitalised interest amount via MOD-001 — all steps completing atomically before the variation is set to active status.

FR-620 — Variation monitoring: the system shall monitor active hardship variations daily and must alert the back-office hardship team via MOD-063 when: a customer misses a varied repayment during the variation period; the variation end date is within 14 days (advance notice to customer and staff); and when the variation period ends (triggering reversion to original terms or assessment for a further variation if needed).

Policies satisfied:

Policy Mode Description
CON-008 — Financial Hardship Policy GATE Hardship applications must be assessed within the statutory timeframe (NZ CCCFA: 10 working days; AU NCCP: 21 days); the system tracks and enforces these deadlines with escalation on breach.
CON-004 — Product Disclosure & Sales Practice Policy AUTO The customer receives a formal variation agreement documenting the new terms, the duration of the variation, and the effect on their total interest payable before the variation is applied.
CRE-004 — Loan Origination Standards LOG All hardship applications, assessments, decisions, and variation agreements are logged for regulatory examination and responsible lending audit.
CON-003 — Vulnerable Customer Policy AUTO Hardship customers are flagged as vulnerable in MOD-007 and all communications are managed with empathy protocols during the variation period.

MOD-142 — Deposit guarantee scheme disclosure

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Manages deposit guarantee scheme disclosure obligations for both NZ and AU jurisdictions — delivering the required disclosure at account opening, inserting a coverage statement in every periodic account statement, displaying a real-time coverage indicator in the app, and optionally notifying customers whose total deposits exceed the scheme limit. Disclosure events are logged for regulatory examination.

Scheme coverage overview

Jurisdiction Scheme Coverage limit Governing legislation Live date
NZ Depositor Compensation Scheme (DCS) NZD 100,000 per natural person per deposit taker Deposit Takers Act 2023 July 2025
AU Financial Claims Scheme (FCS) AUD 250,000 per account holder per ADI Banking Act 1959, Part II Division 2AA Ongoing

Joint accounts under the NZ DCS are apportioned per account holder per the rules set out in MOD-125 (joint account management) — each holder's share of the joint account balance counts toward their individual NZD 100,000 limit. The FCS in Australia applies the coverage limit per account holder and calculates each holder's share of a joint account's balance independently.

What is and is not covered

Deposit type DCS / FCS eligible
Transaction accounts Yes
Savings accounts Yes
Term deposits Yes
Notice saver accounts Yes
Foreign currency deposits No
Managed funds and investment products No
Shares and bonds No
Amounts held in trust accounts Subject to scheme rules — not automatically eligible

The in-app disclosure clearly states that investment products and foreign currency deposits are not covered. The account detail screen shows the coverage indicator only on eligible account types.

Disclosure obligations

Three distinct disclosure touchpoints are required:

Account opening. A full DCS or FCS disclosure — scheme name, coverage limit, eligibility conditions, and a statement that investment products are not covered — must be delivered via MOD-050 before the account activates. The customer must acknowledge the disclosure. The acknowledgement is recorded in app.dcs_fcs_disclosures with a timestamp, and the account activation gate in MOD-050 holds until the record is present.

Periodic statements. Every account statement generated by MOD-113 must include a deposit guarantee scheme insert. The insert states the scheme name, the coverage limit per person, and a note that the customer's total deposits at this institution count toward a single limit per person. The insert is present on every statement regardless of whether the account balance is above or below the coverage limit.

In-app coverage indicator. The account detail screen displays a real-time coverage indicator showing the customer's total eligible deposit balance across all accounts at the institution (including their apportioned share of joint accounts), the scheme limit, the protected amount, and any uncovered amount above the limit. The indicator refreshes within 5 seconds of a balance change.

Coverage calculation

The coverage calculation aggregates all eligible deposit balances held by the natural person at the institution in the applicable jurisdiction:

  1. Retrieve all active eligible accounts held solely by the customer in the relevant jurisdiction.
  2. For joint accounts, retrieve the customer's apportioned share per the joint account configuration in MOD-125.
  3. Sum the real-time available balances from MOD-003 across all eligible accounts and shares.
  4. Compare the total to the scheme limit for the jurisdiction.
  5. Display: total eligible balance, scheme limit, protected amount (min of total and limit), uncovered amount (max of total minus limit, 0).

The calculation runs in the app layer on demand and does not require a backend batch process for the in-app indicator. Statement inserts use the statement-date balance captured at statement generation time.

Data model

-- app.dcs_fcs_disclosures
CREATE TABLE app.dcs_fcs_disclosures (
  disclosure_id       UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  customer_id         UUID NOT NULL,
  account_id          UUID NOT NULL,
  jurisdiction        TEXT NOT NULL CHECK (jurisdiction IN ('NZ','AU')),
  scheme              TEXT NOT NULL CHECK (scheme IN ('DCS','FCS')),
  coverage_limit      NUMERIC(18,2) NOT NULL,
  disclosure_type     TEXT NOT NULL CHECK (disclosure_type IN ('account_opening','statement','in_app_view')),
  acknowledged        BOOL NOT NULL DEFAULT false,
  acknowledged_at     TIMESTAMPTZ,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT now()
);

account_opening rows are created when a new eligible account is opened and updated when the customer acknowledges. statement rows are created by MOD-113 for each statement generation event. in_app_view rows are created each time the customer views the coverage indicator — these are sampled rather than logged for every balance-driven refresh, to avoid unbounded log volume.

Key operations

Account opening disclosure gate. When a new eligible deposit account is created, MOD-050 triggers the DCS/FCS disclosure flow. The disclosure content is rendered from the jurisdiction-specific template (NZ DCS or AU FCS). The account cannot activate until the customer acknowledges. The acknowledgement record is written to app.dcs_fcs_disclosures.

Statement insert generation. On each statement generation event in MOD-113, the statement insert is generated from the current scheme parameters for the account's jurisdiction. The insert is appended to the statement regardless of account balance. A statement row is created in app.dcs_fcs_disclosures for audit purposes.

In-app coverage indicator calculation. The account detail screen requests the coverage calculation from the app layer. The calculation retrieves balances from MOD-003, aggregates across eligible accounts and joint account shares, and returns the covered and uncovered amounts. The result is displayed inline on the account detail screen and refreshes on balance change events.

Threshold alert. When a customer's total eligible deposit balance first exceeds the scheme coverage limit, MOD-063 dispatches a notification advising the customer of the uncovered amount and suggesting they consider spreading deposits across institutions. This notification fires once per calendar year per customer if their balance remains above the limit — it does not fire on every transaction above the threshold.

Requirements

FR-629 — Account opening disclosure gate: the system must deliver a DCS (NZ) or FCS (AU) disclosure via MOD-050 at account opening — including the scheme name, coverage limit, eligibility conditions, and a statement that investment products are not covered — and must block account activation until the customer acknowledges; the acknowledgement must be recorded in app.dcs_fcs_disclosures with a timestamp.

FR-630 — Statement insert: the system must include a deposit guarantee scheme coverage statement in every periodic account statement generated by MOD-113, showing the scheme name, coverage limit per person, and a note that the customer's total deposits count toward a single limit; the insert must be present even when the balance is below the coverage limit.

FR-631 — Real-time coverage indicator: the system must display on the account detail screen the customer's total eligible deposit balance (including their apportioned share of joint accounts from MOD-125), the applicable scheme limit, the protected amount, and any uncovered amount; the indicator must refresh within 5 seconds of a balance change.

FR-632 — Threshold notification: the system must send a notification via MOD-063 when a customer's total eligible deposit balance first exceeds the scheme coverage limit in a calendar year, advising the customer of the uncovered amount and suggesting they consider spreading deposits across institutions; the notification is sent at most once per calendar year per customer.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy GATE DCS (NZ) or FCS (AU) disclosure must be delivered and acknowledged by the customer at account opening before the account activates; disclosure is repeated in account statements and on the account detail screen.
REP-007 — DCS & Depositor Reporting Policy CALC The customer's total protected deposit balance across all eligible accounts is calculated and displayed in-app, enabling the customer to understand how much of their deposits falls within the protection limit.
CON-001 — Customer Fairness & Conduct Policy AUTO The coverage indicator on the account detail screen updates automatically whenever the account balance changes, so the customer always sees their current coverage position without needing to check separately.
REP-004 — Financial Statements Policy LOG DCS/FCS disclosure events are logged for regulatory examination — RBNZ and APRA may inspect whether disclosure obligations have been met at account opening and in statements.

MOD-146 — Restricted activities enforcement

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Prevent a deploying deposit taker from inadvertently enabling product features or activities that are classified as restricted under the NZ Deposit Takers Act 2023 Restricted Activities Standard. The DTA restricts certain non-deposit-taking and non-lending activities for licensed deposit takers unless RBNZ consent or a board resolution with RBNZ notification is in place. This module enforces those constraints at the platform's product configuration layer, ensuring that no restricted activity can be switched on through routine configuration without the required authorisation being on record.

What it does

Restricted activities register. The platform ships with a restricted_activities register — a curated list of product types and feature flags that are classified as restricted under the DTA Restricted Activities Standard. The register is maintained by the platform vendor and updated when the standard is amended by RBNZ. Examples of restricted-activity classifications include: operating a collective investment scheme, underwriting insurance, and acting as a derivatives market maker. Each entry in the register carries the DTA standard reference, a plain-language description of the restricted activity, and the date the classification was last confirmed.

Gate on product configuration. When MOD-127 (product configuration) receives a proposal to enable a product type or feature flag, this module checks the restricted activities register against the proposed item. If the item is classified as restricted, the proposal is automatically set to blocked_pending_authorisation status and cannot advance to the approved or live state until a valid authorisation is attached. The gate operates at both initial product creation and at subsequent feature-flag changes to an existing product.

Authorisation to unblock. To move a blocked proposal out of blocked_pending_authorisation status, the compliance team must attach one of three authorisation types:

  • An RBNZ consent reference number, confirming that RBNZ has provided formal consent for the restricted activity.
  • A board resolution reference, with confirmation that the board has resolved to undertake the activity and that RBNZ has been notified as required by the standard.
  • A platform vendor advisory confirming that the item in question has been reclassified as not restricted in the current version of the standard, applicable where the register update has not yet been deployed.

The authorisation is reviewed and recorded by a compliance team member, not self-certified by the product owner proposing the change.

Authorisations table. The app.restricted_activity_authorisations table records each authorisation with the following columns: authorisation_id, product_type_or_flag, authorisation_type (rbnz_consent / board_resolution / vendor_reclassification), reference_document, authorised_by, authorised_at, expires_at (nullable — used where an RBNZ consent is time-limited), and notes. Where an authorisation has an expires_at date, the product configuration is automatically re-flagged as blocked_pending_authorisation when the authorisation expires, unless a renewal authorisation has been attached.

Audit logging. Every restricted activity check generates a log entry recording: the product type or flag checked, the classification result (restricted / not restricted), the authorisation attached (if any), and the identity of the person who provided the authorisation. These records feed into the MOD-048 decision log and are available for RBNZ examination.

Jurisdiction scope. The restricted activities register and enforcement gate are NZ-only. The module is inactive for AU-only deployments. For NZ + AU deployments, the gate applies only to product types and feature flags that are within scope of the NZ DTA standard; AU-specific regulatory constraints are handled separately.

RBNZ examination export. The restricted activities authorisation log is included in the platform's RBNZ examination data export, ensuring that an examiner can verify that every restricted activity the institution operates has a corresponding authorisation on record.

Compliance reason

The DTA Restricted Activities Standard is a condition of registration for NZ deposit takers. A breach — enabling a restricted activity without the required RBNZ consent or board resolution — is a licensing violation that must be notified to RBNZ and remediated. If the platform has no enforcement mechanism, a misconfiguration by a bank employee could inadvertently create a licence breach with no internal visibility until an external audit or examination reveals it. System-level enforcement at the configuration gate is the strongest possible control because it makes violation structurally impossible without leaving a complete audit trail of who provided what authorisation and on what basis.

Commercial reason

Restricted activities enforcement reduces the deploying institution's compliance risk at the point where it is cheapest to prevent — configuration time, before any customers are affected and before RBNZ notification obligations are triggered. Discovering a restricted activity breach after launch requires regulatory notification, customer remediation, and potential enforcement action. Preventing the breach at the configuration gate has negligible operational cost and eliminates the downside entirely. The authorisation workflow also creates a clean paper trail that reduces the cost of future RBNZ examinations by making compliance evidence readily retrievable.

Policies satisfied:

Policy Mode Description
GOV-010 — Restricted Activities Policy GATE Product types and features classified as restricted activities under the DTA cannot be enabled in the product catalogue without a documented RBNZ consent or board resolution — the system enforces this at the configuration layer.
REP-001 — Regulatory Reporting Policy LOG All restricted activity classification decisions, consent records, and product enablement events are logged as regulatory records.

MOD-148 — Privacy access request (DSAR) workflow

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Provide an end-to-end governed workflow for handling customer privacy access requests (data subject access requests, DSARs) under the NZ Privacy Act 2020 (Information Privacy Principle 6) and AU Privacy Act 1988 (APP 12). The workflow covers intake, identity verification, data assembly across platform systems, decision and disclosure, correction requests, and escalation to the Privacy Commissioner where a request is refused or the statutory SLA is at risk of breach.

What it does

Intake channels

Customers may submit a DSAR through the in-app support interface using the MOD-053 case management form, or a staff member may create a case on the customer's behalf following a phone or branch contact. Each submission creates a privacy_access_request case type in MOD-053 and triggers the DSAR workflow. Duplicate detection runs on submission to identify whether the same customer has an open request of the same type.

Identity verification

Before data assembly begins, the customer's identity must be confirmed. Requests submitted through the authenticated app session satisfy this requirement automatically — the existing MOD-068 session token is recorded as the verification event. For requests submitted via other channels (email, branch, phone), the handling agent must document the verification method used and record the outcome before the workflow advances to assembly. Data assembly is blocked until a verified identity event is present on the case.

SLA timer

The module applies a jurisdiction-appropriate statutory timer from the moment of receipt. For NZ customers, the NZ Privacy Act 2020 requires a response within 20 working days. For AU customers, APP 12 requires a response within 30 calendar days. The correct timer is applied automatically based on the customer's jurisdiction field in their CDD profile (MOD-010). An amber warning notification is generated when 50% of the available SLA time remains. A red escalation alert is sent to the Privacy Officer when 20% of the available time remains. If the deadline is reached without a disclosed or refused outcome, the case is escalated automatically and a Privacy Commissioner notification is prepared for review.

Data assembly

On reaching the assembly stage, the module generates a structured extract of all personal information held about the customer across platform systems. The extract draws from the CDD profile (MOD-010), account and transaction data (MOD-001), loan records, communication logs, marketing preferences, and consent records. Assembly is automated; the resulting package is presented to the privacy team in a structured review interface before any disclosure occurs. The assembling agent may annotate individual data categories and flag items for legal review.

Third-party data redaction

Where the assembled data contains personal information about third parties — for example, joint account holders, authorised signatories, or named payees — the reviewing agent must redact that information before disclosure. The module provides an in-browser redaction tool that allows the agent to black out fields or sections. Redaction actions are logged with the agent identifier, timestamp, and the category of information removed. The redacted package is generated as a separate artefact; the unredacted source is retained internally for audit.

Correction requests

Customers may request correction of personal information they believe to be inaccurate or incomplete. Correction cases follow the same intake and verification steps, with an additional review stage in which the proposed correction is assessed by the data owner for the relevant data category. If the correction is upheld, it is applied and the customer is notified of the change. If declined, the customer is advised of the outcome and their right to complain. The outcome and rationale are recorded on the case.

Refused requests

Where a request is refused — on grounds such as legal professional privilege, harm to a third party, or a statutory exception — the refusing officer must document the specific ground for refusal on the case. A formal refusal letter is generated from a template, incorporating the stated ground and the customer's right to complain to the Privacy Commissioner (NZ: Office of the Privacy Commissioner; AU: Office of the Australian Information Commissioner). Refused cases are flagged for Privacy Officer review before the letter is dispatched.

Data table

The app.privacy_access_requests table holds: request_id, customer_id, request_type (access / correction / erasure_query), received_at, identity_verified_at, jurisdiction (NZ / AU), sla_due_at, status (received / assembling / under_review / disclosed / refused / escalated), disclosed_at, refusal_reason, commissioner_notified.

Compliance reason

NZ Privacy Act 2020 IPP 6 and AU Privacy Act 1988 APP 12 both confer a right on individuals to access personal information held about them by an organisation, and a right to request correction of that information. Failure to respond within the statutory timeframe is a breach of the Act and grounds for a formal complaint to the Privacy Commissioner. Without a governed workflow, DSAR responses are managed ad hoc, with no SLA enforcement, no consistent identity verification gate, and no audit trail of what was disclosed and to whom. The module eliminates all three deficiencies and creates the documentation required for regulator examination.

Commercial reason

Privacy access request volumes are increasing across financial services as customers become more aware of their rights. A manual DSAR process is labour-intensive — assembling data across multiple systems for a single request can take hours. Automated assembly alone eliminates the bulk of that effort. SLA enforcement prevents the escalations to the Privacy Commissioner that are disproportionately damaging to the institution's reputation relative to the cost of responding on time. The governed workflow also protects the institution from inadvertent over-disclosure of third-party data, which creates its own privacy liability.

Policies satisfied:

Policy Mode Description
PRI-001 — Privacy Policy AUTO Customer access requests are received, triaged, and fulfilled within the statutory timeframe — the workflow enforces the SLA and escalates automatically if it is at risk.
PRI-003 — Personal Information Retention & Destruction Policy LOG Every access request, data assembly action, disclosure decision, and regulator escalation is logged as an immutable privacy record.
PRI-006 — Customer Data Access & Correction Policy AUTO DSAR workflow assembles the complete data inventory held about a customer across all platform systems — subject access fulfilment is automated, not manual.

MOD-151 — Risk case console

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

The Risk Case Console is the human interface for exception-driven risk management. Cases arrive exclusively from the Risk Management Platform (MOD-150) when an event requires human judgement: a P1 incident requiring a post-mortem, a model failing its validation gate, a vendor SLA breach needing a remediation plan, a potential regulatory breach awaiting sign-off before notification is submitted, or a whistleblower report requiring investigation.

No risk event is manually entered here. Every case was generated by the automated pipeline. The console provides the workflow to assess, act on, close, and audit the exception — and nothing else.

Case types and routing

Seven case types are handled. All originate from MOD-150 triggers.

1. Incident Triggered by a P1 or P2 alert from MOD-076. P1 incidents are assigned to the on-call engineer and Head of Technology; P2 incidents are assigned to the on-call engineer. SLA timers run from case creation. Root cause documentation is mandatory before closure — the close action is not available until a root cause field has been completed and a resolution action recorded. This gate satisfies OPS-003.

2. Operational risk event Triggered when the risk register auto-classification produces an event that exceeds the configured severity threshold or falls into a category requiring human treatment assessment. Routed to a risk_officer. Requires a treatment decision: accept, mitigate, transfer, or escalate. Treatment documentation is recorded against the case and written to the risk register via MOD-048.

3. Vendor SLA breach Triggered when a critical third-party service breaches its configured SLA and the breach has not auto-resolved within the grace period. Routed to the risk_officer and the relevant business domain owner. Requires a remediation plan within the case SLA. Contractual review reminder cases (generated 90 days before contract expiry) follow the same routing.

4. Model validation request Triggered when a model reaches awaiting_validation status in the MOD-150 model inventory. Assigned to an independent validator — a role that cannot be the same individual as the model owner. The case requires: a validation report upload, a formal approve or reject decision, and a summary of findings. A model cannot be promoted to production in the MOD-150 inventory until this case is closed with an approved status. This gate is enforced at the inventory level by MOD-150, not in application logic here — the case closure is the unlock condition.

5. Regulatory breach review Triggered when MOD-150 assembles a draft regulatory notification. Routed to the compliance_officer. The notification draft — pre-populated with incident detail, affected service, estimated customer impact, and current resolution status — is presented in the case for review. The compliance officer can approve the draft for submission, amend it, or escalate to the CCO. Where MOD-150 has submitted via a regulator API, the case records the submission timestamp and confirmation reference.

6. Whistleblower intake Described in full below.

7. Change post-implementation review Triggered for any deployment that resulted in a rollback or that was followed by a P1 incident within 72 hours. Routed to the tech lead for the relevant module. Requires a documented review outcome covering: what was deployed, what failed, what the root cause was, and what change to process or testing is being made. The outcome is recorded against the change record in risk.change_records via MOD-048.

Whistleblower intake channel

The whistleblower channel is a distinct intake path, not a case type added to the standard queue.

Access: Available at a public URL (no authentication required) and within the authenticated app under a clearly labelled "Protected disclosure" link. Both paths accept anonymous submissions.

Routing: Submissions are routed exclusively to the board_audit_committee role. No management role — including CEO, COO, CFO, CRO, CCO, Head of Technology, or any team lead — can view a whistleblower case. This isolation is enforced at the database role level in Neon: whistleblower cases are stored in a separate schema (risk.whistleblower_cases) with column-level encryption on submitter identity fields. The application access control layer cannot override this — even if a bug existed in the application's role check, the query would return no data for a non-board_audit_committee role.

Notification: The Board Audit Committee chair receives a notification via a secure, out-of-band channel when a new case is submitted. This notification does not go through MOD-063 (which routes notifications through shared infrastructure visible to operations staff).

Submitter protections disclosed: Before a submission is completed, the submitter is shown a plain-language summary of: the NZ Protected Disclosures (Protection of Disclosers) Act 2022 protections, the AU Corporations Act Part 9.4AAA protections for eligible whistleblowers, and the platform's own identity protection commitment. The submitter must confirm they have read this before the submission is accepted.

Follow-up: The submitter receives a case reference number on submission. They can return to the public URL at any time, enter their reference number, and add further information — anonymously if they originally submitted anonymously. The Board Audit Committee can ask questions through this same channel without learning the submitter's identity unless the submitter chooses to disclose it.

Role-based access

Role Case types visible Actions permitted
on_call_engineer Incident Update status, add notes, close (with root cause)
tech_lead Incident, Change PIR Update status, add notes, close
risk_officer Operational risk event, Vendor SLA breach, Incident (read) Treatment decision, remediation plan, escalate
compliance_officer Regulatory breach review, Incident (read) Approve/amend notification, escalate to CCO
model_validator Model validation request Upload report, approve/reject
board_risk All except Whistleblower Read-only, plus RAF dashboard view
board_audit_committee Whistleblower only Full case management
internal_audit All (read-only) No write actions
cro All except Whistleblower Escalation target; read-only on cases; can reassign

Role assignment is managed through MOD-068. The board_audit_committee role is issued only to board members holding that committee position and is reviewed at each board composition change.

RAF dashboard view

Users with the board_risk or risk_officer role see a dashboard panel at the top of the console showing:

  • All open risk cases by type and current SLA status (green / amber / red)
  • Current RAF indicator status pulled live from MOD-150 (green / amber / red per indicator)
  • Model inventory summary: count of production models, count with upcoming review dates, count flagged
  • Vendor health summary: count of critical providers, count currently in breach or incident

This gives the risk function a single place to see the platform's risk posture without navigating Snowflake directly. The underlying data is always MOD-150's live computation — the console displays it, it does not store it.

SLA enforcement

Follows the same pattern as MOD-053. Each case type has a configured resolution SLA. At 50% elapsed: amber warning visible on the case and on the assignee's dashboard. At 80% elapsed: red escalation notification sent to the assignee's manager. At 100% elapsed: auto-escalation to the next tier (CRO for risk cases, CCO for compliance cases, Head of Technology for incident and change cases). The escalation is recorded in the case audit trail.

Compliance rationale

GOV-008 (NZ Protected Disclosures (Protection of Disclosers) Act 2022) requires a formal protected disclosure procedure that ensures a discloser can make a protected disclosure without fear of retaliation. The Act imposes obligations on the organisation to have a procedure, to keep the discloser's identity confidential, and to not take adverse action against the discloser. AU equivalent protections exist under Corporations Act Part 9.4AAA for eligible whistleblowers. A policy document alone does not satisfy these obligations — the protection must be technically enforced. Database-level isolation of submitter identity fields means the protection cannot be accidentally or intentionally defeated by application-layer changes.

OPS-003 (RBNZ Operational Resilience Standard, APRA CPS 230) requires a documented incident management process with root cause analysis for material incidents. The mandatory root-cause-before-close gate creates a verifiable, auditable record that the requirement was met for every P1 incident — not just the ones that a risk manager chose to document.

DT-005 (APRA CPS 220 model risk) requires independent validation of models before production use. The validation case type operationalises this: the model owner cannot approve their own model, the CI/CD hook enforces the gate, and the audit trail shows precisely when validation was performed and by whom.

GOV-006 (Internal Audit Policy) requires that the internal audit function has access to records needed to assess control effectiveness. The internal_audit read-only role, combined with the no-deletion guarantee, satisfies this requirement structurally.

Commercial rationale

Whistleblower protections that exist only in a policy document are not protections at all. An employee who suspects a colleague of fraud — including a senior colleague — needs to be able to report it without their manager seeing the report. If the system routes the report through the same notification infrastructure that operations staff monitor, or if a determined administrator can query the database directly to identify the submitter, the protection is illusory. The technical isolation described above makes it real.

Risk cases that exist in email threads or shared spreadsheets lose their audit trail, cannot have SLA enforcement applied to them, and cannot be reviewed systematically by internal audit. The console creates a single governed record for every exception, with a complete history of every action taken and by whom. This is not a compliance formality — it is what allows the risk function to demonstrate, under regulatory examination, that identified risks were assessed and acted upon within the required timeframes.

Policies satisfied:

Policy Mode Description
OPS-003 — Incident Management Policy GATE All P1 incidents require a documented root cause and resolution action before they can be closed — the case workflow enforces this gate and no bypass path exists.
GOV-008 — Whistleblower Protection Policy GATE Whistleblower submissions are received through an isolated intake channel with no management routing; cases are delivered directly to the Board Audit Committee role and identity protection is enforced at the data layer.
GOV-006 — Internal Audit Policy LOG All risk cases, decisions, and resolutions are available to the internal_audit role for examination — no case can be deleted.
DT-005 — Model Risk Management Policy GATE Model validation cases enforce an upload-report-then-approve gate; a model cannot be promoted in the MOD-150 inventory without a closed validation case with an approved validation report attached.

MOD-155 — Target Market Determination (AU DDO)

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

The Target Market Determination module provides the TMD management capability required to comply with the Australian Design and Distribution Obligations (DDO) under Corporations Act Part 7.8A and ASIC RG 274. For every AU-distributed retail financial product, the module stores the approved TMD, monitors distribution outcomes against the target market description, detects defined trigger events, and escalates for TMD review. Distribution outside the target market is detected automatically and reported as a conduct event.

What it does

TMD record store

Each AU product has a TMD record in app.target_market_determinations: tmd_id, product_id, approved_at, approved_by (compliance_officer role), review_date, status (current/under_review/expired), target_market_description (JSONB — structured criteria for the class of retail clients for whom the product is appropriate), distribution_conditions, review_triggers (configurable list), and version (monotonically incrementing). A compliance_officer approval is required before status is set to current. No AU product can be activated in MOD-127 for distribution without a current TMD. If the TMD expires or is set to under_review, the product is automatically suspended from new distribution until a fresh approval is recorded.

Distribution event monitoring

For each AU product sale or referral, a distribution event is recorded and the acquiring customer's characteristics from MOD-010 are evaluated against the structured target market criteria in the TMD. Criteria may include: customer age range, risk tolerance, financial literacy indicator, investment horizon, and specific product-eligibility flags. Events where the customer falls outside the target market are flagged as out_of_market_distribution and stored with the specific criteria mismatch. A configurable threshold of out-of-market events within a rolling review period triggers a TMD review.

Trigger detection and review workflow

The module monitors each product's configured trigger conditions: complaint rate threshold, product return/early redemption rate, out-of-market distribution count, a distribution channel incident, or a material change to product terms or target customer profile. When a trigger threshold is met, a TMD review case is created in MOD-053, assigned to the Head of Product, and requires compliance_officer re-approval before the case can be closed. During an active TMD review, the product remains distributable but all new distribution events are flagged for enhanced monitoring.

ASIC significant dealings reporting

Significant out-of-target-market dealings — individual transactions above a configured value threshold or systemic patterns — are compiled into a structured report for ASIC submission under the DDO reporting obligation. Submission tracking records the submission date and ASIC acknowledgement reference.

Compliance reason

The Australian DDO (Corporations Act Part 7.8A, effective October 2021) requires every issuer of a retail financial product to: make a TMD, take reasonable steps to ensure distribution is consistent with the TMD, monitor distribution, act on review triggers, and report significant out-of-target-market dealings to ASIC. ASIC has already taken enforcement action against issuers for DDO failures. With 23 products planned and AU distribution intended from launch, the TMD obligation applies to every AU retail product from day one.

Commercial reason

The DDO review trigger mechanism is commercially valuable beyond compliance: a high complaint rate or high early-redemption rate on a product is early evidence of distribution to the wrong customers. Automatic trigger detection allows the institution to correct its distribution strategy before the commercial damage compounds and before ASIC initiates an inquiry.

Policies satisfied:

Policy Mode Description
CRE-008 — Product Design & Distribution Policy GATE No AU retail product can be distributed without an approved, current TMD on file — distribution is blocked at the product configuration layer; no bypass path.
CON-006 — Product suitability and governance AUTO Customer characteristics are automatically evaluated against the TMD target market criteria for each product sale; out-of-target-market distribution events are detected and recorded without manual review.
CON-001 — Customer Fairness & Conduct Policy LOG Out-of-target-market distribution events and TMD trigger breaches are logged as conduct events and escalated for review.

MOD-164 — Facility component self-service

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

Purpose

Provides the customer-facing self-service interface in the bank app for managing components within a Flexible Loan Facility (PRD-024). Exposes four primary flows: facility overview, component rollover, add component, and partial prepayment. All state-changing flows involving a fixed-rate component require a binding break-cost disclosure acknowledgement via MOD-050 before the action is submitted.

Context

The product research document (Section 4.3, Phase 2) identifies self-service component management as the feature that differentiates the FLF from a simple fixed-rate loan. BNZ retail customers commonly operate in this mode; NAB Business Markets Loan customers rely on it for cost-effective rate management. Without in-app self-service, every component rollover requires a banker-assisted interaction — operationally expensive and a poor customer experience for what should be a routine event.

This module is the app layer only. No component data is stored here; everything reads from MOD-162 and the break-cost service MOD-163. State changes are submitted to MOD-162 APIs. The module's own scope is the UI flows, the disclosure gate wiring, and the break-cost display logic.

Flows

Facility and component overview

The landing screen for a customer's Flexible Loan Facility shows:

  • Total facility limit and expiry date
  • Current outstanding balance (sum of all active component principals)
  • Principal-weighted effective interest rate (from MOD-162)
  • Each active component as a card: component type (FIXED/FLOATING), principal, rate (or benchmark + margin), maturity date (FIXED only), days to maturity, indicative break cost (fetched from MOD-163 on screen load — displayed as a cost or benefit)
  • A call-to-action per component: Roll component (approaching maturity), Add component (from floating), Prepay (fixed components)

Indicative break-cost figures on the overview screen are informational only — no acknowledgement is required to view them. They are refreshed on each screen load from MOD-163.

Rollover flow

Available from 90 days before a fixed component's maturity date, and at any time for early rollover.

  1. Customer selects a fixed component and taps Roll.
  2. The app displays the current indicative break cost (if rolling early) and the available rate menu for the replacement component (fixed terms and current rates from MOD-162 configuration).
  3. Customer selects rate type, term, and principal allocation for the new component.
  4. If rolling early (before maturity): the app calls MOD-163 for a binding break-cost calculation. MOD-050's requireDisclosure() gate is invoked with the binding calculation ID. The customer must acknowledge the break-cost disclosure before proceeding.
  5. If rolling at maturity: no break cost applies. MOD-050 issues a rate-election disclosure (new rate and terms) that must be acknowledged before confirmation.
  6. On confirmation, the rollover instruction is submitted to MOD-162 (update-component-status + create-component).

If no rollover is elected before maturity, the component automatically converts to the floating residual (handled by MOD-162's daily maturity sweep). The app sends a notification via MOD-063 at 90, 60, and 30 days before maturity.

Add component

Allows the customer to convert a portion of the floating residual into a new fixed-rate component.

  1. Customer taps Add component from the facility overview.
  2. App shows the available floating residual balance and the rate menu.
  3. Customer selects principal amount, rate, and term. The principal must be ≥ the configured minimum component size and must not exceed the current floating residual balance.
  4. MOD-050 requireDisclosure() is invoked for the new component terms (rate, total interest, break-cost mechanics). Customer acknowledges.
  5. On confirmation, the instruction is submitted to MOD-162 (create-component), which reduces the floating residual and recomputes the effective rate.

Partial prepayment

Allows the customer to reduce the principal of a fixed-rate component early (or repay it in full).

  1. Customer selects a fixed component and taps Prepay.
  2. Customer enters the prepayment amount (partial or full).
  3. App calls MOD-163 for a binding break-cost calculation on the nominated amount.
  4. MOD-050 delivers the break-cost disclosure and captures acknowledgement. The disclosure must present: the break cost or benefit amount, the formula inputs (contracted rate, current market rate, remaining term), and a plain-language explanation of what the break cost represents.
  5. On acknowledgement, the prepayment instruction is submitted to MOD-162 (update-component-status), which reduces the component principal and increases the floating residual. If full prepayment, the component status transitions to PREPAID.
  6. Funds for any break cost are collected at the time of prepayment via MOD-001. A break benefit is credited to the customer's nominated account.

Notifications

MOD-063 is invoked by this module (or by MOD-162 events) to dispatch:

  • 90/60/30-day pre-maturity alerts prompting the customer to elect a rollover
  • Rollover confirmation notification after a component is successfully rolled
  • Prepayment confirmation including break cost amount paid or break benefit received
  • Effective-rate change notification when the principal-weighted rate shifts

Implementation notes

All break-cost figures displayed in the UI must originate from MOD-163 — no client-side approximation. The indicative figure shown on the overview screen and the binding figure confirmed in a change flow must use the same formula and the same market rate source. A visual discrepancy between the two (due to rate movements between screen load and confirmation) must be explained to the customer rather than silently updating the figure.

The add-component and rollover flows must handle the concurrent-edit case: if a second device or a background system event changes the floating residual between the customer seeing the available balance and submitting the component creation, the MOD-162 handler must reject with a LIMIT_EXCEEDED or INSUFFICIENT_FLOATING error and the app must prompt the customer to refresh and retry.

Rate quotes in the add-component and rollover flows have a validity window (configured in MOD-162, default 15 minutes). If the customer takes longer than the validity window, the quote must be refreshed before the instruction can be submitted.

Policies satisfied:

Policy Mode Description
CON-005 — Fee & Pricing Transparency Policy GATE Prepayment and rollover flows in the self-service UI route through MOD-050 for binding break-cost acknowledgement before any fixed-rate component change is submitted — no UI path completes a fixed-rate component change without a confirmed acknowledgement record.
CRE-009 — Fixed-Rate Component Break-Cost Methodology Policy LOG On-demand indicative break-cost quotes are accessible to any authenticated customer with an active facility at any time via the component detail screen, satisfying the on-demand quotation transparency requirement without requiring a formal change request.

MOD-177 — SD06 risk dashboard renderer

System: SD08 | Repo: bank-app | Build status: Not started | Deployed: No

The SD06 risk dashboard renderer is the back-office TypeScript/Vite interface for SD06 regulatory and risk intelligence dashboards. It renders Recharts-based visualisations for capital adequacy, liquidity, model risk, and operational risk data sourced exclusively from the Snowflake semantic layer via MOD-176.

Rendering approach

React/Recharts components are authored using a Claude Code session that reads the SD06 module's semantic view definitions, dbt column descriptions, and wiki module spec as context. The session produces typed TypeScript components that the developer reviews and commits. Generated components are ordinary source code — version-controlled, editable, and treated identically to hand-written code. There is no runtime code generation and no metadata layer.

New or updated dashboard components for a module are produced by opening a Claude Code session pointing at: - The module's CREATE SEMANTIC VIEW DDL (bank-risk-platform migrations) - The module's dbt schema.yml column descriptions - The module's wiki page (requirements, capability list, data model section)

Data access

All data queries are structured metric requests sent to MOD-176 POST /v1/snowflake/metrics. The renderer holds no SQL, no Snowflake credentials, and no direct Snowflake connection. A component binds to a metric name, a set of dimensions, and optional filters — the query shape is declared in the component, resolved by Cortex Analyst at request time.

Write-backs

Back-office actions that change state (model parameter overrides, validation approvals, alert threshold changes) are submitted as maker-checker proposals to MOD-168, not written directly. The actor identity is taken from the Cognito JWT claim (ADR-065). Classification:

Action Tier Time-lock
Model parameter override TIER-3 15 min
Change control approval (MOD-175) TIER-3 15 min
Alert threshold change TIER-2 None

The SiS (Streamlit in Snowflake) renderer maintained per SD06 module is independent of this module. Divergence between the two renderers is accepted — they serve different audiences and are not coordinated.

Incremental delivery

MOD-171 and MOD-172 are deployed and provide the first dashboard panels at launch. MOD-173, MOD-174, and MOD-175 panels are added as each module reaches Built status — the renderer degrades gracefully without them (routes return a "not yet available" state rather than failing).

Policies satisfied:

Policy Mode Description
DT-001 — Information Security Policy GATE All Snowflake metric queries and write-back requests pass through authenticated, TLS-terminated internal API calls — no Snowflake credentials or database credentials exist in the browser.
GOV-003 — Three Lines of Defence Policy CHECKER Consequential back-office actions (model parameter overrides, change control approvals) are submitted as MOD-168 proposals requiring a second authorised reviewer before execution — self-approval blocked at the database layer.

SD09 — Brand & Public Surfaces

Repo: bank-brand | Business domain: BD04 | Tech owner: Platform Engineering | Build status: Not started

Purpose

The public-internet face of Totara Bank — marketing site, regulatory disclosures (when added), brand assets, and contact channels. Served from a static-generated codebase deployed to Cloudflare Pages. No authentication, no internal API consumption, no SSM dependencies beyond the CI/CD infrastructure that builds and deploys it.

Architecture

Static-first site (Astro) with minimal reactive islands (React) for the contact form and mobile navigation. Cloudflare Pages for hosting; Cloudflare Pages Functions for the contact-form mail relay (MailChannels). No bank-internal dependencies — keeps the public surface decoupled from internal infrastructure outages and reduces attack surface.

Critical constraints

  1. Standalone — no SSM reads, no IAM beyond the Cloudflare deploy token, no calls to internal services. Anything dynamic (live rates, balances) links into the customer app instead of rendering server-side.
  2. KISS — single static-site generator (Astro), one styling system (Tailwind), zero CMS, content in version control as MDX.
  3. Reactive only where it matters — contact form is a React island; everything else is server-rendered HTML for performance and indexability.
  4. No cookies, no analytics initially — no banner needed. If either is added later, the privacy notice / cookie banner becomes mandatory before launch.

Hostname

totara.global (public DNS managed by Cloudflare). Pages project bank-public-website for the deploy target.

Modules in SD09


MOD-169 — Public website

System: SD09 | Repo: bank-brand | Build status: Deployed | Deployed: Yes

Purpose

Static public marketing website for Totara Bank, served from totara.global. Entry point for prospective customers, partners, and regulators (when regulatory content is added later).

What it does

  • Product overview pages (savings, transactional, credit) — copy only, no live rates
  • About / team / company information
  • Contact-us form delivering to a marketing ops inbox
  • Brand surface (logo, palette, typography) shared with future public properties

What it does not do

  • No authentication
  • No reads of live banking data
  • No API calls to internal bank services
  • No cookies or analytics initially
  • No CMS — content is in MDX files in version control

Technical approach

  • Astro static site generator, with React islands for the contact form and mobile navigation
  • Tailwind for styling
  • MDX content collections for product pages
  • Cloudflare Pages for hosting; Cloudflare Pages Functions for the contact-form mail relay
  • MailChannels (free outbound email from Cloudflare) for the contact-form sink, routing to a fixed marketing inbox
  • Cloudflare Turnstile for contact-form spam protection

Local development

pnpm install && pnpm devhttp://localhost:4321 with hot reload. Same source tree the CI build uses; no Docker, no env setup beyond a .env.example template.

CI/CD

Stages: install → validate → test → build → deploy → smoke. Custom marketing.gitlab-ci.yml template — not extending frontend.gitlab-ci.yml (which carries app-specific complexity unneeded here).

Secrets

Cloudflare CI variables only (masked in project settings): CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, TURNSTILE_SECRET_KEY, CONTACT_FORM_TARGET_EMAIL. No AWS Secrets Manager, no bank SSM parameters.

Policies satisfied:

(No policies assigned)


AI context — generated 2026-05-22 by scripts/compile.py. Not in site nav.