Skip to content

MOD-121 — Construction loan drawdown engine

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

Purpose

Operates the progressive-drawdown lifecycle of a construction mortgage: tranche-by-tranche disbursement gated on quantity-surveyor milestone certification, interest accrual on drawn balance only, per-drawdown LVR recalc via MOD-115, and automatic conversion to P&I via MOD-112 when all milestones complete or construction_end_date is reached.

FRs

  • FR-545 — Drawdown disbursement prevented unless the corresponding tranche has status certified; enforced both at the data layer (CHECK constraint chk_construction_tranches_drawn_state) and the handler layer (TRANCHE_NOT_CERTIFIED).
  • FR-546 — Each approved drawdown is posted as a MOD-001 PAYMENT (DEBIT loan_account / CREDIT customer deposit) AND total_drawn + loan_accounts.outstanding_principal updated in the same DB transaction. Atomic via postDrawdownAtomically.
  • FR-547 — Daily accrual base for MOD-005 is loan_accounts.outstanding_principal, kept in sync with total_drawn in the same drawdown transaction. No separate MOD-005 API call needed (CON-005 — accrue on drawn only — by-design).
  • FR-548 — When all tranches reach status='drawn' OR construction_end_date is reached (whichever first), schedule.status flips to complete and bank.credit.construction_phase_completed is emitted. MOD-112 (amortisation) and MOD-063 (customer notification) consume this event.

Architecture

   create-schedule (URL/IAM)        ── back-office attaches schedule + tranches
   certify-tranche (URL/IAM)        ── records QS cert: pending|inspection_requested → certified
   request-drawdown (URL/IAM)       ── FR-545 GATE + FR-546 saga
   daily-expiry-sweep               ── cron(0 21 * * ? *) UTC = 09:00 NZST
                                       └ catches construction_end_date crossings

   Synchronous integrations:
     MOD-001 PAYMENT (k-5)              — ledger posting per tranche

   Event-driven downstreams (no HTTP calls):
     bank.credit.construction_schedule_created     → MOD-063 (welcome notice)
     bank.credit.construction_milestone_certified  → MOD-063 (cert confirmation)
     bank.credit.construction_drawdown_posted      → MOD-063 (drawdown notice), audit
     bank.credit.loan_balance_updated              → MOD-115 (LVR refresh; k-8)
     bank.credit.construction_phase_completed     → MOD-112 (amortisation schedule), MOD-063 (final notice)

13 AD ratifications (k-1..k-13)

AD Decision Why
k-1 loan_account_id everywhere; legacy loans.loan_id prose ignored Same convention as MOD-116/117
k-2 Construction loans are product_type='MORTGAGE' (existing enum); no new product_type value Wiki says "converts to a standard residential mortgage"
k-3 Separate MOD-121 create-schedule back-office Function URL called AFTER MOD-029 approves the mortgage MOD-029 stays construction-unaware; same pattern as MOD-115 register-property-security
k-4 The initial mortgage_rate_period is created at origination by the back-office workflow (out of scope here); MOD-116 elect-rate becomes available only after conversion Decouples MOD-121 from MOD-116
k-5 MOD-001 posting_type='PAYMENT', reference='CONSTRUCTION_DRAWDOWN_T<n>'; DEBIT loan_account, CREDIT customer deposit Same as MOD-116 discharge pattern
k-6 total_drawn kept in sync with loan_accounts.outstanding_principal in the same txn as the drawdown MOD-005 reads outstanding_principal directly; no second API
k-7 MOD-112 trigger via event bank.credit.construction_phase_completed MOD-112 is event-driven (rate changes, restructures etc.) — same pattern
k-8 LVR refresh via bank.credit.loan_balance_updated event consumed by MOD-115 consume-loan-balance-updated (already built) No new MOD-115 call needed
k-9 MOD-063 notification via event consumption MOD-063 owns its inbound event filter — we just emit
k-10 No per-drawdown CON-005 disclosure Loan disclosed at MOD-029 origination; amortisation update is the FR-548 CON-004 disclosure
k-11 daily-expiry-sweep at 09:00 NZST (UTC 21:00); DISABLED in non-prod Slots after MOD-115/116/117
k-12 Idempotency keys: mod121:drawdown:{schedule_id}:{tranche_number}, mod121:expiry-sweep:{YYYY-MM-DD} Stored in shared credit.idempotency_keys; PAY-001 AUTO
k-13 Contract package + CI publish job day-1; no cross-repo notify (no listed consumers in v1) Generated via scripts/contract-add.py MOD-121

Policies satisfied

  • CRE-002 GATE — drawdown requires status='certified'; enforced at both layers (DB CHECK + handler). Tests pol-cre-002-gate.test.ts (3 assertions).
  • CRE-001 CALC — LVR recalc per drawdown via emitted loan_balance_updated event consumed by MOD-115.
  • CON-005 CALC — Interest accrues on drawn only; loan_accounts.outstanding_principal mirror keeps MOD-005's accrual base honest.
  • CON-004 AUTO — Updated amortisation schedule dispatched after each drawdown via MOD-063 (event-driven).
  • PAY-001 AUTO — Stable idempotency key per (schedule_id, tranche_number) for MOD-001 PAYMENT; daily-expiry-sweep idempotent on per-day key. Tests pol-pay-001-auto.test.ts.

Data model

credit.construction_schedules (NEW; mutable)

Master record per construction loan. loan_account_id UNIQUE — one schedule per mortgage.

column type notes
id uuid PK
loan_account_id uuid NOT NULL UNIQUE FK loan_accounts(id) k-3 paired record
total_facility numeric(18,2)
total_drawn numeric(18,2) ≥ 0, ≤ total_facility k-6 mirror of outstanding_principal
construction_end_date date
conversion_date date NULL set on completion or expiry
status text CHECK (active, complete, defaulted)
jurisdiction, currency, source_application_id, trace_id, created_at, updated_at

credit.construction_tranches (NEW; mutable)

Per-milestone tranche. UNIQUE (schedule_id, tranche_number). State machine pending → inspection_requested → certified → drawn (or → lapsed).

Key CHECK constraints: - chk_construction_tranches_drawn_statedrawdown_date + posting_id valid only when status='drawn'. (DB-layer FR-545.) - chk_construction_tranches_certified_state — certification fields required for status ∈ {certified, drawn, lapsed}.

credit.construction_events (NEW; Cat 1 immutable)

Append-only lifecycle audit via shared credit.fn_immutable_row() trigger.

SSM outputs

path value
/bank/{stage}/credit/construction/create-schedule-{function-arn,function-name,api-endpoint}
/bank/{stage}/credit/construction/certify-tranche-{function-arn,api-endpoint}
/bank/{stage}/credit/construction/request-drawdown-{function-arn,api-endpoint}
/bank/{stage}/credit/construction/daily-expiry-sweep-function-arn
/bank/{stage}/credit/tables/construction-{schedules,tranches,events}/name

Filed handoffs

  1. MOD-121-complete.handoff.md — wiki status In progress; 5 new outbound events; 3 new tables.
  2. MOD-001-api-contract-export.handoff.md — request to bank-core: publish /api subpath on @bank-core/mod-001-contracts so consumers don't have to inline the PostingRequest/Response types.

Dependency status (all Deployed)

Dep Repo Status
MOD-001 (GL posting) bank-core Deployed
MOD-005 (daily accrual) bank-core Deployed
MOD-112 (amortisation) bank-core Deployed
MOD-063 (notifications) bank-platform Deployed
MOD-115 (LVR) bank-credit Deployed
MOD-065 (loan_accounts) bank-credit Deployed
MOD-128 (shared bootstrap) bank-credit Deployed