Skip to content

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_id nullable)
  • 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_keys keyed 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_register and 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.