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_detectedwithin 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:
- MOD-116 calls MOD-001 → receives final-repayment posting_id.
- MOD-116 calls MOD-115 discharge URL with posting_id as idempotency_key.
- 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. FiledMOD-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.