Skip to content

MOD-115 — Property security and LVR management

System: SD05 — Credit Decisioning & Loan Platform Repo: bank-credit Status (at handoff time): In progress → Built → Deployed (CI-driven)

Purpose

Property-specific LVR engine for residential mortgages. FR-521/522/523/524:

  • FR-521 — Register property security at settlement; GATE drawdown if security absent or LVR exceeds policy.
  • FR-522 — Daily LVR snapshot per loan; band classification; available to MOD-033/MOD-036 within 60 min.
  • FR-523 — Emit bank.credit.lvr_breach_detected within 5 min of detection (event-triggered path is primary).
  • FR-524 — Atomic security discharge with the final loan repayment posting in MOD-001 (best-effort saga in v1; idempotent on posting_id).

Architecture

                                   MOD-066 emits
                                   bank.credit.collateral_revalued
                                   ┌─────────────────────────────┐
                                   │ EB rule (same-bus) + SQS    │
                                   └──────────────┬──────────────┘
                                   consume-collateral-revalued
   register-property-security ───┐                 │
   (URL/IAM, called by MOD-116)  │                 │
                                 │                 │
   check-lvr (URL/IAM) ──────────┤   reads/writes  │
   (FR-521 GATE)                 ▼                 ▼
                          ┌─────────────────────────────────────┐
                          │ credit.property_security_details    │
                          │   (sidecar; one-to-one collateral)  │
                          │                                     │
   discharge-property-    │ credit.lvr_snapshots (Cat 1)        │
   security (URL/IAM) ───▶│                                     │
   (FR-524)               │ Reads MOD-066:                       │
                          │   credit.collateral_register        │
                          │   credit.collateral_valuations      │
                          │ Writes MOD-066:                      │
                          │   collateral_register status=RELEASED│
                          └───────────────┬─────────────────────┘
                              Outbound (3 events):
                                bank.credit.property_security_registered
                                bank.credit.lvr_breach_detected
                                bank.credit.security_discharged

   daily-lvr-sweep (cron 18 UTC = 06:00 NZST) ── catch-up snapshots
   consume-loan-balance-updated (SQS, facility_status_changed) ── BALANCE_CHANGE

FRs satisfied

FR Mode Implementation
FR-521 GATE check-lvr Function URL — MOD-116 calls before drawdown; returns allowed=true/false
FR-522 CALC/LOG daily-lvr-sweep cron + Cat 1 immutable lvr_snapshots; available to MOD-033/MOD-036 within 60 min via Snowflake CDC
FR-523 ALERT event-triggered path (5-min SLA) emits lvr_breach_detected + SNS-alarms; daily sweep is the catch-up
FR-524 LOG discharge-property-security URL marks collateral_register RELEASED + emits security_discharged; idempotent on posting_id (k-9 best-effort saga)

Data model

credit.property_security_details (NEW; sidecar to MOD-066)

Mutable. One-to-one with MOD-066's credit.collateral_register row (PK = collateral_id). UNIQUE (loan_account_id) enforces v1 one-property-per-loan assumption (k-5).

column type notes
collateral_id uuid PK FK collateral_register(id)
loan_account_id uuid NOT NULL FK loan_accounts(id) UNIQUE k-5 v1 one-property
title_reference text NOT NULL LINZ / LRS title ref
property_address jsonb NOT NULL {street, suburb, city, postcode, country}
property_subtype text NOT NULL CHECK RESIDENTIAL / RURAL_RESIDENTIAL / APARTMENT / TOWNHOUSE
borrower_intent text NOT NULL CHECK OWNER_OCCUPIER / INVESTOR (RBNZ BS19 differentiator)
registration_number_lr text NULL LR-specific number
trace_id uuid NOT NULL
created_at, updated_at timestamptz touch trigger

credit.lvr_snapshots (NEW; Cat 1 immutable)

Append-only daily history. Regulatory snapshots must not change after taking. UNIQUE (loan_account_id, snapshot_date) is the DB-layer dedup guard.

column type notes
id uuid PK
loan_account_id, collateral_id uuid NOT NULL FK
snapshot_date date NOT NULL
outstanding_balance, current_valuation numeric(18,2) NOT NULL
lvr_pct numeric(7,4) NULL NULL only when current_valuation = 0
lvr_band text NOT NULL CHECK <60 / 60-70 / 70-80 / 80-90 / >90
policy_max_lvr numeric(7,4) NOT NULL locked at snapshot time
policy_breach boolean NOT NULL
jurisdiction char(2) NOT NULL NZ/AU
borrower_intent text NOT NULL denormalised
trigger_reason text NOT NULL CHECK DAILY_SWEEP / REVALUATION / BALANCE_CHANGE / REGISTRATION
trace_id uuid NOT NULL
created_at timestamptz

Lambdas

name trigger purpose
register-property-security Function URL (AWS_IAM) FR-521 register; MOD-116 calls at drawdown
check-lvr Function URL (AWS_IAM) FR-521 GATE pre-check
discharge-property-security Function URL (AWS_IAM) FR-524 discharge; idempotent on posting_id
daily-lvr-sweep EB Scheduler cron(0 18 * * ? *) UTC = 06:00 NZST FR-522 catch-up
consume-collateral-revalued SQS+EB rule on bank.credit.collateral_revalued FR-523 5-min SLA
consume-loan-balance-updated SQS+EB rule on bank.credit.facility_status_changed BALANCE_CHANGE recompute

Methodology

Raw LVR vs MOD-066 LTV

MOD-066 computes haircut-adjusted LTV for credit-risk monitoring. MOD-115 computes raw (un-haircut) LVR for regulatory band reporting — RBNZ BS19 + APRA APS 220 bands are computed on the raw figure.

Policy max LVR (k-7)

OWNER_OCCUPIER NZ = 0.80   (RBNZ BS19 baseline)
INVESTOR       NZ = 0.70   (RBNZ BS19 investor cap)
OWNER_OCCUPIER AU = 0.80
INVESTOR       AU = 0.70

CFO governance — fold into the existing combined CFO sign-off (already covers PD/LGD/Stage 3/haircut/LTV-product-policy/MOD-066 haircut). Filed addendum handoff.

LVR bands (k-7)

<60        lvr <  0.60
60-70      0.60 ≤ lvr < 0.70
70-80      0.70 ≤ lvr < 0.80
80-90      0.80 ≤ lvr < 0.90
>90        lvr ≥ 0.90  (also covers null lvr)

Strict > on the breach comparison: LVR exactly at policy max is not a breach. Treasury can tighten via AppConfig override.

Cure semantics

bank.credit.lvr_breach_detected is emitted only on false → true transitions. Cure (true → false) is observable by downstream consumers via the next bank.credit.collateral_revalued event from MOD-066 or — within MOD-115 — via the lvr_snapshots row's policy_breach=false. Same pattern as MOD-066 collateral_revalued.

FR-524 saga (k-9)

True cross-database atomicity is not available in v1 (bank_credit + bank_core). Best-effort saga:

  1. MOD-116 calls MOD-001 → receives final-repayment posting_id.
  2. MOD-116 calls MOD-115 discharge URL with posting_id as idempotency_key.
  3. MOD-115 marks collateral_register RELEASED (idempotent), publishes security_discharged.

If step 3 fails post step-1, finance ops alarm fires; manual remediation completes the saga. v2 introduces a proper outbox pattern.

SSM outputs

/bank/{stage}/credit/property-security/register-api-endpoint
/bank/{stage}/credit/property-security/check-lvr-api-endpoint
/bank/{stage}/credit/property-security/discharge-api-endpoint
/bank/{stage}/credit/property-security/{*}-function-arn (×6)
/bank/{stage}/credit/tables/property-security-details/name
/bank/{stage}/credit/tables/lvr-snapshots/name

Idempotency

  • Function URLs: credit.idempotency_keys (k-9 discharge keyed by posting_id)
  • Daily sweep: mod115:lvr-sweep:{YYYY-MM-DD}
  • SQS consumers: keyed by inbound event_id
  • DB-level: UNIQUE (loan_account_id, snapshot_date) on lvr_snapshots; UNIQUE (loan_account_id) on property_security_details

Risks + open items

  • MOD-085 property index feed — v1 ships without it. AVM/index-driven revaluation is deferred until MOD-085 adds market.property_indices. Filed MOD-085-property-index-feed.handoff.md.
  • MOD-116 integration — MOD-116 must call check-lvr (FR-521 GATE) and discharge URL (FR-524). Filed MOD-116-LVR-gate-integration.handoff.md.
  • CFO sign-off on LVR policy + bands — fold into the existing combined CFO governance memo. Filed addendum.
  • Wiki schema gaps — k-1/k-2 amendments already applied via the orchestrator; residual data-model-gap handoff covers anything that remained out of sync.
  • v2 multi-property mortgages — k-5 v1 one-property assumption enforced via UNIQUE (loan_account_id) on the sidecar; v2 drops it.