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 constraintchk_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_principalupdated in the same DB transaction. Atomic viapostDrawdownAtomically. - FR-547 — Daily accrual base for MOD-005 is
loan_accounts.outstanding_principal, kept in sync withtotal_drawnin 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'ORconstruction_end_dateis reached (whichever first), schedule.status flips tocompleteandbank.credit.construction_phase_completedis 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). Testspol-cre-002-gate.test.ts(3 assertions). - CRE-001 CALC — LVR recalc per drawdown via emitted
loan_balance_updatedevent consumed by MOD-115. - CON-005 CALC — Interest accrues on drawn only;
loan_accounts.outstanding_principalmirror 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_state — drawdown_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¶
MOD-121-complete.handoff.md— wiki status In progress; 5 new outbound events; 3 new tables.MOD-001-api-contract-export.handoff.md— request to bank-core: publish/apisubpath on@bank-core/mod-001-contractsso 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 |