MOD-066 — Collateral & security management¶
System: SD05 — Credit Decisioning & Loan Platform Repo: bank-credit Status (at handoff time): In progress → Built → Deployed (CI-driven)
Purpose¶
Operational register of assets pledged as security against credit exposures. Per FR-197/198/199/200:
- FR-197 — Record every security interest with type, registered value, registration ref, expiry date.
- FR-198 — Alert credit-ops 90/30/7 days before registration expiry; create a renewal task in the work queue.
- FR-199 — Recalculate LTV on every valuation update; flag breaches against product policy max.
- FR-200 — Prevent write-off of secured exposure until realisation completed or explicitly waived by an authorised officer with documented rationale. v1 ships alarm-based detection (k-8 option a); the proper GATE is filed as a MOD-065 follow-on handoff.
Architecture¶
┌──────────────────────────┐
register-collateral (URL/IAM) ──┐ │ credit.collateral_ │
│ │ register (mutable) │
record-valuation (URL/IAM) ─────┼──── reads/ │ │
│ writes │ credit.collateral_ │
request-release (URL/IAM) ──────┤ │ valuations (Cat 1) │
│ │ │
approve-release (URL/IAM) ──────┘ │ credit.collateral_ │
... FR-200 GATE (k-3 state machine) │ release_requests │
│ (mutable; app-layer │
│ immutable terminal) │
│ │
daily-expiry-sweep │ credit.collateral_ │
cron 0 17 * * ? * UTC = 05:00 NZST │ renewal_tasks (mutable) │
(FR-198 — 90/30/7 day windows) └──────────────────────────┘
consume-facility-status (SQS) ←── bank.credit.facility_status_changed
FR-200 alarm-based detection (k-8 option a)
Outbound (6 events, all NEW vs wiki):
bank.credit.collateral_registered
bank.credit.collateral_revalued ← cure observable via
previous_ltv_breach + ltv_breach
bank.credit.collateral_ltv_breached ← only on false → true transition
bank.credit.collateral_released
bank.credit.collateral_renewal_due (90/30/7 day FR-198)
bank.credit.collateral_writeoff_violation (FR-200 k-8 alarm)
Functional requirements satisfied¶
| FR | Mode | Implementation |
|---|---|---|
| FR-197 | LOG | register-collateral handler + credit.collateral_register row + collateral_registered event |
| FR-198 | LOG | daily-expiry-sweep cron + credit.collateral_renewal_tasks queue + renewal_due event + SNS alarm |
| FR-199 | CALC | record-valuation handler + computeLtv (pure) + collateral_valuations Cat 1 audit + revalued/ltv_breached events |
| FR-200 | GATE/LOG | request-release + approve-release Function URLs (k-3 PENDING → APPROVED |
Data model¶
credit.collateral_register (k-1 amendments)¶
Mutable. Wiki-spec'd baseline plus k-1 amendments:
| column | type | notes |
|---|---|---|
| jurisdiction | char(2) NOT NULL CHECK (NZ, AU) | k-1 |
| haircut_pct | numeric(5,4) NOT NULL CHECK ≥ 0 AND < 1 | k-1 (strict; 1.0 would zero net) |
| net_collateral_value | numeric(18,2) NOT NULL | k-1 |
| product_policy_max_ltv | numeric(8,6) NOT NULL | k-1; locked at registration |
| ltv_ratio | numeric(8,6) NULL | k-1 |
| ltv_breach | boolean NOT NULL DEFAULT false | k-1 |
| registration_expiry_date | date NULL | k-1; FR-198 alerting |
| registration_authority | text NULL | k-1; e.g. PPSR-NZ, PPSR-AU, LINZ |
| registration_reference | text NULL | k-1 — renamed from ppsr_registration_number |
Touch trigger maintains updated_at. Status flips: ACTIVE → PENDING_RELEASE → RELEASED (or back to ACTIVE on REJECTED).
credit.collateral_valuations (NEW — k-2)¶
Append-only Cat 1 immutable. Per-revaluation row.
| column | type | notes |
|---|---|---|
| id, collateral_id, valuation_date, estimated_value, valuation_source | basics | |
| currency | char(3) NOT NULL | k-2 amendment — snapshot self-contained |
| haircut_pct_applied, net_collateral_value | computed at valuation time | |
| outstanding_principal_at_valuation | k-11 — captured at the time | |
| ltv_ratio, ltv_breach, product_policy_max_ltv | ||
| model_version, valued_by_staff_id, trace_id | FR-187-style audit |
UNIQUE (collateral_id, valuation_date, valuation_source) is the dedup guard.
credit.collateral_release_requests (NEW — k-3)¶
Mutable; application-layer immutability for terminal states. 3-state machine (no COMPLIANCE_BLOCK transition path in v1). Biconditional CHECK on the row enforces:
status = 'PENDING' AND approved_by_staff_id IS NULL AND decided_at IS NULL
OR
status IN ('APPROVED','REJECTED') AND approved_by_staff_id IS NOT NULL AND decided_at IS NOT NULL
Approve-release handler enforces PENDING → APPROVED|REJECTED at the API edge with COMPLIANCE_BLOCK on terminal-row updates.
credit.collateral_renewal_tasks (NEW — k-4)¶
Mutable. UNIQUE (collateral_id, days_until_expiry, expiry_date) —
the k-4 amendment includes expiry_date so renewed registrations don't
collide with the prior cycle's COMPLETED task.
days_until_expiry CHECK IN (90, 30, 7)
status CHECK IN ('PENDING','IN_PROGRESS','COMPLETED','CANCELLED')
Lambdas¶
| name | trigger | purpose |
|---|---|---|
| register-collateral | Function URL (AWS_IAM) | FR-197 — register pledge |
| record-valuation | Function URL (AWS_IAM) | FR-199 — revaluation + LTV recompute |
| request-release | Function URL (AWS_IAM) | FR-200 — open release request |
| approve-release | Function URL (AWS_IAM) | FR-200 GATE — k-3 state machine |
| daily-expiry-sweep | EB Scheduler cron(0 17 * * ? *) UTC = 05:00 NZST |
FR-198 — 90/30/7 day windows |
| consume-facility-status | SQS (EB rule on bank.credit.facility_status_changed) | FR-200 k-8 alarm |
Key methodology¶
Haircut defaults (k-7)¶
Locked v1 defaults (treasury-overridable via AppConfig):
REAL_PROPERTY = 0.20
VEHICLE = 0.40
TERM_DEPOSIT = 0.05
GUARANTEE = 0.50 (CFO sign-off needs to clarify personal vs institutional split — k-7 amendment)
BUSINESS_ASSETS = 0.50
OTHER = 0.50
Product-policy max LTV (k-12)¶
PERSONAL_LOAN = 1.00
CREDIT_LINE = 1.00
OVERDRAFT = 1.00
MORTGAGE = 0.80 (RBNZ BS19 owner-occupier baseline)
BUSINESS_LOAN = 0.70
v2 introduces credit.product_policies table — wiki-todo handoff.
LTV calculation¶
net_collateral_value = estimated_value × (1 - haircut_pct)
ltv_ratio = outstanding_principal / net_collateral_value
ltv_breach = ltv_ratio > product_policy_max_ltv
Edge case: when net_collateral_value = 0 (estimated_value = 0), ltv_ratio is null and ltv_breach = true if outstanding > 0.
Cure semantics (k-6 NOTE)¶
bank.credit.collateral_revalued carries previous_ltv_breach and
ltv_breach. A cure (true → false) is observable to consumers via that
pair on a revalued event; no separate cure event is emitted in v1.
The dedicated bank.credit.collateral_ltv_breached event fires only on
false → true transitions.
FR-200 alarm-based detection (k-8 option a)¶
consume-facility-status subscribes to bank.credit.facility_status_changed
on the bank-credit bus. On WRITE_OFF_PENDING transitions where active
collateral exists without an APPROVED release_request:
- publishes
bank.credit.collateral_writeoff_violation(k-6 amendment payload:loan_account_id,collateral_id,release_request_idnullable) - SNS-alarms MOD-076 alarm-intake topic
MOD-066 does not block the write-off. The proper GATE is filed in
MOD-065-collateral-writeoff-precheck.handoff.md — the v2 target is a
credit.fn_loan_has_open_collateral(loan_account_id) Postgres function
that MOD-065's V00x migration adds as a CHECK.
SSM outputs¶
/bank/{stage}/credit/collateral/register-api-endpoint
/bank/{stage}/credit/collateral/record-valuation-api-endpoint
/bank/{stage}/credit/collateral/request-release-api-endpoint
/bank/{stage}/credit/collateral/approve-release-api-endpoint
/bank/{stage}/credit/collateral/{*}-function-arn (×6)
/bank/{stage}/credit/tables/collateral-{register,valuations,release-requests,renewal-tasks}/name
Idempotency (k-10)¶
- Function URLs:
credit.idempotency_keyskeyed by request idempotency_key. - Daily expiry sweep:
mod066:expiry-sweep:{YYYY-MM-DD}. - SQS consumer: keyed by inbound
event_id. - DB-level guards (defence-in-depth):
- UNIQUE
(collateral_id, valuation_date, valuation_source)on collateral_valuations - UNIQUE
(collateral_id, days_until_expiry, expiry_date)on collateral_renewal_tasks (k-4)
Risks + open items¶
- GUARANTEE haircut split — k-7 CFO sign-off must clarify personal vs institutional.
- FR-200 GATE — v1 is alarm-based. Real GATE depends on MOD-065 follow-on.
- Customer self-service app — out of v1 (k-9). SD08 follow-on.
- Wiki data-model gap — k-1 amendments to
collateral_registerand 3 NEW tables + 6 NEW events not yet in the wiki SD05 data model / event catalogue. Filed handoff. - Working-day calendar — already filed as part of MOD-031 wiki todo; affects the 90/30/7 day windows when expiry falls on a non-BD.