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¶
ledger_entriestable is append-only — enforced by database trigger. Corrections via reversal entries only.- Balance checks for payment authorisation must read from this system — never from Snowflake.
- Interest accrual must complete for all accounts by 23:59 daily (NFR-006).
- 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 postingprevious_available_balance+available_balance— available balance before and afterposting_id— the causal posting reference, used as the idempotency sequencetrace_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_mirrorwithstatus = 'VERIFIED'. Multi-party account types (trust, community, joint) bypass the single-party KYC mirror gate when the activation is initiated with an approvedreason_codefrom the multi-party module (see below). - ACTIVE → RESTRICTED: Requires a
restriction_reasonfrom the allowed domain. - RESTRICTED → ACTIVE: Reinstatement — triggered by MOD-007 FR-440 reinstatement flow.
- ACTIVE / RESTRICTED → DORMANT: Triggered when
last_transaction_atcrosses 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 GETsprimary-deposit-account-url. - MOD-117 (bank-credit) — On
consecutive_drawn_days ≥ 60, POSTs tohardship-flag-set-urlwithflagged_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¶
- Variable rate change (MOD-006 event received) — new instalment amount calculated from outstanding balance, remaining term, new rate; schedule regenerated from next payment date.
- 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.
- Interest-only period expiry — schedule transitions from interest-only payments to principal and interest; instalment amount recalculates over remaining P&I term.
- Hardship restructure (from MOD-065) — new schedule generated from restructured terms; original schedule retained for audit; both stored as separate schedule versions.
- Loan variation (from MOD-132) — new schedule generated from confirmed variation terms (new rate, term, frequency, rate type); prior
is_current=trueschedule superseded. SeePOST /internal/v1/loans/{account_id}/recalc-variationbelow.
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:
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_idnotcustomer_id—core.member_register.party_idisUUID NOT NULLwith no Postgres FK (cross-domain boundary; mirrors the pattern inaccounts.account_party_relationships).core.customersdoes not exist in bank-core's DB.- Uppercase enums — all
CHECKenum values are uppercase per SD04 standard. core.share_transactionsis 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 whereallwould 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:
- Completed eIDV and received a KYC status of
verified. - 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:
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:
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:
- Identify the entity type and obtain the governing document (constitution or rules)
- Identify all committee officers with authority to transact on the account (the authorised signatories)
- Complete individual eIDV (via MOD-009) for each of those signatories before the account activates
- 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¶
- 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
- Create a
core.community_accountsrecord withkyc_status = pending - Initiate individual eIDV via MOD-009 for each signatory — each signatory receives their own eIDV flow
- Block account activation until all signatories have
kyc_status = verified - Once all signatories are verified, set the account to active and log the activation to MOD-047
Annual committee change workflow¶
- An existing active signatory (with authority to manage the account) initiates a committee refresh
- They upload the updated authority resolution to MOD-073
- For each outgoing signatory: set
valid_until = today; the signatory loses transaction authority immediately upon save - For each incoming signatory: create a new
core.community_signatoriesrecord withkyc_status = pending; trigger eIDV via MOD-009 - The incoming signatory gains transaction authority only after eIDV is verified
- 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_reviewstatus 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 tobalance × (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_amountas 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¶
- MOD-009 is a hard GATE — account state machine (MOD-007) will not activate any account without
kyc_status = Verified. - MOD-013 is a hard GATE — no payment can proceed for a confirmed sanctions match under any circumstances.
- All KYC decisions must be written to the immutable audit trail (MOD-012) before the decision takes effect.
- Sanctions list re-screening must run automatically within 1 hour of any list update.
- No Snowflake calls inline — customer risk score is read from the
risk_scores_mirrorPostgres 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_statusbefore any Pending → Active transition. Ifkyc_statusis notVerified, 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
Related pages¶
- SD02 system index
- MOD-010 CDD tier assignment
- MOD-013 Sanctions screener
- AML-003 policy
- BD01 Customer domain
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¶
- MOD-020 is a hard GATE — no payment may proceed unless validation passes.
- MOD-013 (SD02) sanctions check must clear before any outbound cross-border payment.
- Balance authorisation must read from Postgres (SD01), never from Snowflake.
- 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.xuk_open_banking— OBIE/FAPI 1.0 Advancedeu_psd2— Berlin Group NextGenPSD2 v1.3generic_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.
Security and consent enforcement¶
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_settlementsin Snowflake - Match to payment records — every settlement item is matched to the corresponding
paymentsrecord 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_exceptionstable 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:
- 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.
- Affordability assessment: customer consents to share transaction history. MOD-027 (affordability) receives normalised transaction data for income/expense analysis without screen-scraping.
- 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.
Consent and data handling¶
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:
- MOD-020 pre-flight checks — balance, sanctions screening, daily payment limits.
- MOD-023 fraud score — BPAY-specific rules applied.
- Debit account via MOD-001 (double-entry).
- Submit to sponsor bank BPAY API; record
sponsor_referenceand set status tosubmitted.
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:
- Credit account via MOD-001.
- Set status to
returnedand recordreturn_reason_code. - 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¶
- Customer enters a PayID value — or BSB and account number — in MOD-071 (payment initiation).
- 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.
- Name confirmation — the resolved display name is shown to the customer: "You are paying [Display Name]." The customer must explicitly confirm.
name_confirmed = trueis set before any funds are committed or any network submission is made. This step is not skippable. It is a Scam-Safe Accord requirement. - 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.
- Post debit via MOD-001 → submit NPP message via sponsor bank → record
npp_message_idand set status toprocessing. - Completion notification dispatched via MOD-063 within 30 seconds of submission.
Inbound Osko receipt¶
- Sponsor bank delivers an inbound NPP credit notification to the module.
- The module resolves the destination account via PayID lookup or direct BSB/account match.
- Credit posted via MOD-001 immediately.
- 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):
- Credit account via MOD-001.
- Set status to
returnedand recordreturn_reason. - 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¶
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:
- File format detection — file extension and header row determine format (ABA or CSV); ambiguous files are rejected
- Encoding — UTF-8 required; BOM is accepted and stripped
- Row count — must be at least 1 detail record; files with zero payment rows are rejected
- Account number format — per-jurisdiction: AU BSB+account validated against the BSB directory; NZ 16-digit format validated against the bank/branch prefix list
- 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
- ABA hash total — checked for ABA files (see above)
- 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: uploaded → validating → pending_approval (customer confirmation step) → processing → settled 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:
- 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.
- A biller registration request is submitted to the sponsor bank, including the biller name, nominated account BSB and account number, and CRN validation configuration.
- The sponsor bank registers the biller with BPAY Pty Ltd. This process typically takes 3–5 business days from submission.
- Once the sponsor bank returns the assigned biller code, the
payments.bpay_billersrecord is updated frompending_registrationtoactiveand 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:
- 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_paymentswith statusreceived. - CRN validation — each payment's CRN is validated against the registered biller's CRN format. Items that fail validation are set to
returnedand reported to the operations queue. - 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.
- Notification — a payment received notification is dispatched to the biller via MOD-063 within 60 seconds of the posting completing.
- 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. Theunavailableresult 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 thefraud_analystrole 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¶
- MOD-027 and MOD-028 must run on write-back values from Snowflake (SD06) — never call Snowflake inline during a customer request.
- All credit decisions must satisfy responsible lending obligations under CCCFA (NZ) and NCC (AU) before any credit is extended.
- 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: VARIABLE → FIXED → EXPIRING → EXPIRED → VARIABLE | 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:
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:
- Select all
overdraft_daily_accrualsrows for the facility whereposted = false. - Sum
daily_interestacross all selected rows. - Post a single debit entry to the account via MOD-001 for the summed amount, with description identifying the period.
- Mark all selected accrual rows
posted = true. - Insert a
interest_chargedevent intooverdraft_events. - Emit
bank.credit.overdraft_interest_chargeddomain 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:
- MOD-117 inserts a
hardship_flagevent intooverdraft_eventswith the consecutive-days count inevent_data. - MOD-007 sets the
hardship_review_pendingflag on the account. - 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.
- 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):
- MOD-007 sets the
unarranged_overdraftstate on the account. - MOD-063 dispatches an immediate customer notification.
- MOD-117 creates a flag record for manual operations review.
- If the bank's product rules permit an unarranged overdraft fee (configured in MOD-110), the fee is posted via MOD-001.
- 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 facilitymilestone_description— plain-text description, e.g. "Frame and roof complete"status— lifecycle state:pending→inspection_requested→certified→drawn→lapsedcertification_dateandcertifier_reference— quantity surveyor or building inspector referencedrawdown_dateandposting_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¶
Consent check¶
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¶
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_accountsrow withproduct_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:
- MOD-124 —
payments.physical_cardsextended withcard_type = 'credit'option,credit_facility_id UUID(links to this module's facility record), andbin_range_type(sponsor|principal). - 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¶
- Snowflake is the analytical store only — no operational system may query Snowflake inline during a customer request.
- All risk model outputs must be written back to Postgres before operational systems consume them.
- MOD-036 prudential returns must be submitted by RBNZ/APRA deadlines without manual intervention.
- 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-041 —
HAS_DCM=false/HAS_DBT_PROJECT=falseworkaround skips the Snowpark deploy step, soNORMALISE_MERCHANTUDF is never deployed. Resolution: flipHAS_DCM=trueandHAS_DBT_PROJECT=true— the Snowpark path should deploy the UDF automatically once the flags are restored. -
MOD-056 —
HAS_DCM=falseworkaround prevented Phase-2 ownership transfer from running. After flippingHAS_DCM=true, the expected next blocker is schema ownership: theREGULATORYschema is owned byBANK_NONPROD_RISK_ROLE, notSF_ROLE; dbt creating views in that schema asSF_ROLEwill fail with "Insufficient privileges". Resolution options: (A) extend Phase-2 inrisk-platform.gitlab-ci.ymlto also transfer schema ownership, or (B) add explicitGRANT 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_currentDynamic Table hastarget_lag='5 minutes'while its upstream dependencytrial_balance_periodhastarget_lag='15 minutes'— Snowflake requires downstream DTs to have lag ≥ their dependencies. Fix: bumpreconciliation_status_current.sqltarget_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.
Navigation from hub¶
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
COREdatabase 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:
- Receiving property-level net income/loss from MOD-094 at period end
- For profitable properties: recording the profit as normal income
- For loss-making properties: ring-fencing the loss into a carry-forward register; it is not available to offset other income
- Applying portfolio-level offset: ring-fenced losses from Property A can offset profits from Property B in the same portfolio
- 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¶
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¶
Activated modules are defined inbilling.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):
Included thresholds are defined inbilling.tenant_tiers (the tier the licensee subscribed to).
Infrastructure passthrough (optional)¶
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. |
MOD-147 — Related party exposure monitor¶
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¶
Related party registry¶
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.
Compliance link¶
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):
- Change-control record is created with classification = material or major
- MOD-175 generates the RBNZ submission artefact package (change description, impact analysis, re-validation evidence) in the format the RBNZ expects
- The deployment gate in the NZ customer's pipeline is locked
- The customer submits to RBNZ and records the approval reference in MOD-175
- 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¶
- 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.
- MOD-044 JWT RBAC is a hard GATE — no system or agent may access data without a valid scoped token.
- MOD-047 must log every agent action with actor, target, action type, and timestamp — no exceptions.
- 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_resultview 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:
- Provision
BILLING.TENANT_MODULESandBILLING.TENANT_TIERSinMOD-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). - Add
BILLING.INVOICESandBILLING.INVOICE_LINE_ITEMStables to the sameinfra/snowflake/directory. - Remove
billing.sqlandbilling_*.sqlfromMOD-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. UseCREATE TABLE IF NOT EXISTSin MOD-099's scripts so a re-run against an already-populated schema is safe. - Update
dbt/models/MOD-098-cost-attribution-engine/sources.ymlto reference thebilling.*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: DRAFT → ISSUED → PAID / 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.
Consent model¶
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¶
| 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-developers → BANK_DEVELOPER_ROLE; bank-snowflake-analysts → BANK_ANALYTICS_ROLE; bank-snowflake-reporting → BANK_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:
- Connects to the domain database on the target branch using the
migrate_userdirect connection - Checks the
flyway_schema_historytable for applied versions - Applies pending migrations in version order within a transaction
- Fails fast — any error halts the deploy pipeline before Lambda code is deployed
- 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.pyscript must be re-run and the generated files committed whenever a module'sdependencieschange 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:
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:
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:
- SST deploys MOD-158 resources (Lambda function, IAM role, DynamoDB version table)
- SST Custom Resource triggers the seed Lambda
- Lambda checks
/{repo}/{stage}/seed-versionin SSM - If current version is already recorded → exit immediately (idempotent)
- 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_idcomposed ofdate + account_id + sequenceis 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:
- CloudWatch dashboard (
bank-acceptance-{stage}): one row per scenario, pass/fail status, last run time, duration - Custom metric
E2EAcceptanceSuitePass(namespaceBankPlatform/Acceptance): 1 if all enabled scenarios passed, 0 if any failed - 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¶
- MOD-049 consent capture is a hard GATE — no product feature may be activated without recorded customer consent.
- MOD-050 disclosures must be rendered before any credit or investment product is offered.
- MOD-052 role-scoped access must enforce the principle of least privilege — back office agents see only what their role requires.
- MOD-054 call recordings must be stored in compliance with PRI-001 and CON-007 retention requirements.
Modules in SD08¶
MOD-049 — Open banking consent management¶
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¶
Consent object model¶
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.
Consent capture by profile¶
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.
Consent validation API¶
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.
Customer consent management UI¶
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. |
MOD-070 — Transaction history & search¶
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-mintedpayment_idand full disclosure fieldsapp.payment_initiation_events— Cat 1 immutable (INSERT only); one row per state transition; carriessession_id,device_fingerprint,mod_020_payment_id,mod_020_decision,trace_idapp.payees— mutable address book; soft-deleted;first_use_completeddrives FR-355 gateapp.scheduled_payments— mutable instruction store;next_run_atindex 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; carriesparty_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:
- Power of Attorney (PoA) — a general authority granted by the account holder while they have legal capacity. Takes effect immediately on registration.
- 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.
- 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¶
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.
Legal framework¶
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):
- 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.
- Any existing PoA or EPoA on the deceased's accounts is set to
revoked_by_deathvia MOD-126. PoA ceases by operation of law on the death of the grantor; this action makes the legal position explicit in the system. - The account state is updated to
deceased_estatevia MOD-007. - 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:
-
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.
-
Authority document upload — the LPR uploads the relevant authority document to MOD-073:
- Grant of probate (executor)
- Letters of administration (administrator)
-
Small estate affidavit or statutory declaration (small estate pathway)
-
Back-office review — a back-office staff member reviews both the eIDV result and the uploaded document. The review requires:
- Confirming the grant/letters are addressed to the person claiming to be the LPR.
- Confirming the document references the correct deceased person (name, date of death).
- Confirming the document is not expired or superseded.
-
A two-step approval: the reviewing agent approves, and a supervisor countersigns before access is activated.
-
Access activation — once both approvals are complete, the LPR is granted
estate_accessscope 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:
- The LPR (typically a family member) still completes eIDV via MOD-009.
- In lieu of probate, the LPR submits a statutory declaration or affidavit of claim confirming:
- Their identity and relationship to the deceased.
- 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).
- That no other person has a competing claim to the funds.
- Back-office approval is required — a single approver (no supervisor countersign required for small estates below threshold).
- 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:
- 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.
- 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.
- 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:
- Creates the
hardship_applicationsrecord withstatus = received. - Sets
assessment_due_datebased on the jurisdiction of the account. - Creates a case in MOD-053 with
case_type: hardship_application, linking theapplication_id. - Emits a
bank.app.hardship_application_receivedevent.
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_dateifstatusis stillreceivedorunder_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:
- MOD-112 recalculates the amortisation schedule under the varied terms.
- MOD-050 delivers the formal variation agreement (including the revised schedule) to the customer.
- MOD-007 sets the hardship flag on the account and transitions the account state to
hardship_variation. - For capitalisation variation types: MOD-001 posts the capitalised interest amount to the principal balance.
- The
hardship_variationsrecord is set tostatus = activeandconfirmed_atis 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 tostatus = completedand 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:
- Retrieve all active eligible accounts held solely by the customer in the relevant jurisdiction.
- For joint accounts, retrieve the customer's apportioned share per the joint account configuration in MOD-125.
- Sum the real-time available balances from MOD-003 across all eligible accounts and shares.
- Compare the total to the scheme limit for the jurisdiction.
- 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.
- Customer selects a fixed component and taps Roll.
- 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).
- Customer selects rate type, term, and principal allocation for the new component.
- 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. - 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.
- 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.
- Customer taps Add component from the facility overview.
- App shows the available floating residual balance and the rate menu.
- 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.
- MOD-050
requireDisclosure()is invoked for the new component terms (rate, total interest, break-cost mechanics). Customer acknowledges. - 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).
- Customer selects a fixed component and taps Prepay.
- Customer enters the prepayment amount (partial or full).
- App calls MOD-163 for a binding break-cost calculation on the nominated amount.
- 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.
- 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. - 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¶
- 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.
- KISS — single static-site generator (Astro), one styling system (Tailwind), zero CMS, content in version control as MDX.
- Reactive only where it matters — contact form is a React island; everything else is server-rendered HTML for performance and indexability.
- 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 dev → http://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.