MOD-117 — Overdraft management engine
System: SD05 — Credit Decisioning & Loan Platform
Repo: bank-credit
Status (at handoff time): In progress → Built → Deployed (CI-driven)
Purpose
Operates the post-approval overdraft lifecycle on top of an existing
deposit account. FR-529/530/531/532:
- FR-529 — Create an overdraft facility on an active deposit account; the facility carries an approved limit, an interest rate, a monthly facility fee, and a 12-month review date. CRE-002 GATE: a PASS affordability assessment is required. CON-005 GATE: caller must assert disclosure was delivered.
- FR-530 — Adjust the facility's
current_limit. Increases require a fresh affordability assessment + disclosure ack (k-3). Decreases are caller-driven, but cannot go below the current drawn balance.
- FR-531 — Daily simple-interest accrual (
drawn × rate / 365) for every active facility with a negative balance. End-of-month aggregation posts a single MOD-001 ADJUSTMENT per facility per period (k-9).
- FR-532 — Hardship counter: 60 consecutive drawn days raises a CON-008 ALERT and emits
bank.credit.overdraft_hardship_flag. The counter resets to 0 only after 5 consecutive positive days (k-9 grace window). Monthly facility fee assessment fires on the same monthly-close run; MOD-110 stub controls waiver.
Architecture
create-facility (URL/IAM) ──────┐
adjust-limit (URL/IAM) ──────┤ credit.overdraft_facilities
get-available-balance (URL/IAM) ──────┤ credit.overdraft_daily_accruals
update-drawn-balance (URL/IAM) ──────┤ credit.overdraft_events (Cat 1)
(k-5 stub for MOD-003 feed) │ ↑↓
▼ ↓
shared services + stores
↑ │
daily-accrual-sweep │ │
cron 20 UTC = 08:00 NZST │ │
│ │
monthly-close │ │
cron 14 UTC on day-1 of month │ │
│ │
monthly-close orchestration calls: │ │
MOD-001 ADJUSTMENT (k-9 ref= │ │
OVERDRAFT_INTEREST + OVERDRAFT_FACILITY_FEE) │
MOD-110 fee waiver stub (always returns │
waived=false until MOD-110 ships, k-13) │
Outbound (7 events, all NEW):
bank.credit.overdraft_facility_created
bank.credit.overdraft_limit_changed (direction: increase|decrease)
bank.credit.overdraft_interest_charged
bank.credit.overdraft_fee_assessed (waived: bool)
bank.credit.overdraft_hardship_flag (CON-008 ALERT)
bank.credit.overdraft_unarranged_detected (CON-008 ALERT)
bank.credit.overdraft_facility_closed
AD ratifications (k-1 .. k-13)
| AD |
Decision |
Why |
| k-1 |
account_id is a cross-domain ref to bank_core (no FK) |
Neon multi-DB; same pattern as MOD-065 |
| k-2 |
Paired-record invariant: every overdraft facility has a credit.loan_accounts row (product_type='OVERDRAFT') created in one txn |
One canonical loan record for downstream MOD-030/031/065 consumption |
| k-3 |
Limit increase requires affordability_assessment_id + disclosure_acknowledged=true; decrease requires neither |
CRE-002 (CCCFA s9C / NCC s130) — increase = additional credit |
| k-4 |
last_assessment_id captured on facility + refreshed on every limit increase |
Audit chain — every active limit traces to an assessment |
| k-5 |
current_drawn_balance is a stub for MOD-003's real-time balance feed; updated atomically with loan_accounts.outstanding_principal |
Hot-path balance not callable from MOD-003 yet — same mirror pattern as risk_scores_mirror |
| k-6 |
overdraft_events is Cat 1 immutable (UPDATE/DELETE blocked by fn_immutable_row) |
Same pattern as collateral_valuations / collections_actions |
| k-7 |
unarranged_detected audit event written on every drawn>limit transition |
CON-008 ALERT lock-in |
| k-8 |
7 outbound events (all new vs the wiki catalogue); wiki addendum filed |
Domain catalog needs new event types — see complete handoff |
| k-9 |
NZ rate=18.900%, AU=16.500%; HARDSHIP_DRAWN_DAYS=60; RESET_GRACE_DAYS=5; MOD-001 type=ADJUSTMENT (not PAYMENT — month-end accrual is a GL adjustment, not a customer payment) |
CFO sign-off handoff filed; ADJUSTMENT is the correct semantic per MOD-001 |
| k-10 |
SNS audit-only mode for hardship + unarranged + accrual-sweep failure alerts |
Same v1 stub as MOD-116; replaced by MOD-063 |
| k-11 |
Daily accrual sweep at 08:00 NZST (UTC 20:00); monthly close on day-1 at UTC 14:00; both DISABLED outside prod |
Sequenced after MOD-115/MOD-116 sweeps |
| k-12 |
Synchronous update-drawn-balance Function URL until MOD-003 SQS feed ships |
Test harness + simulator path; replaced by SQS consumer |
| k-13 |
MOD-110 fee waiver checked before MOD-001 fee posting; v1 stub always returns waived=false |
FR-532 hardship branch must be invocable from day 1 |
FRs satisfied
| FR |
Mode |
Implementation |
| FR-529 |
LOG |
create-facility paired-record insert; emits facility_created + limit_set audit |
| FR-530 |
GATE |
adjust-limit enforces increase requires PASS affordability + disclosure (k-3); decrease blocked below drawn balance |
| FR-531 |
CALC |
daily-accrual-sweep populates overdraft_daily_accruals; monthly-close sums + rounds banker's HALF_EVEN + posts MOD-001 ADJUSTMENT |
| FR-532 |
ALERT/CALC |
hardship counter (60 drawn days, 5 grace) + monthly facility fee with MOD-110 waiver branch |
Data model
credit.overdraft_facilities (NEW; mutable)
State: active → suspended → closed. UNIQUE (loan_account_id) and UNIQUE
(account_id) enforce one-overdraft-per-deposit-account.
| column |
type |
notes |
| id |
uuid PK |
|
| loan_account_id |
uuid NOT NULL UNIQUE FK loan_accounts(id) |
k-2 paired record |
| account_id |
uuid NOT NULL UNIQUE |
k-1 cross-domain ref |
| party_id |
uuid NOT NULL |
|
| approved_limit |
numeric(18,2) |
upper-bound; never lowered without close |
| current_limit |
numeric(18,2) |
0 ≤ current ≤ approved |
| current_drawn_balance |
numeric(18,2) ≥ 0 |
k-5 stub mirror of MOD-003 |
| consecutive_drawn_days |
int ≥ 0 |
FR-532 counter |
| consecutive_positive_days |
int ≥ 0 |
k-9 reset window tracker |
| hardship_flagged_at |
timestamptz NULL |
first 60-day crossing timestamp |
| interest_rate_pct |
numeric(8,5) |
0 ≤ rate < 1 |
| facility_fee |
numeric(18,2) |
monthly fee; ≥ 0 |
| currency, jurisdiction |
char(3)/char(2) |
NZD/AUD; NZ/AU |
| status |
text CHECK (active, suspended, closed) |
|
| review_date |
date |
annual review |
| activated_at, closed_at |
timestamptz |
closed_at NULL until close |
| last_assessment_id |
uuid NOT NULL |
k-4 audit chain |
| disclosed_at |
timestamptz NOT NULL |
CON-005 |
| trace_id, created_at, updated_at |
|
touched by trigger |
credit.overdraft_daily_accruals (NEW; mutable on posted flip)
UNIQUE (facility_id, accrual_date) — FR-530 idempotency at the DB layer.
| column |
type |
notes |
| id |
uuid PK |
|
| facility_id |
uuid NOT NULL FK overdraft_facilities |
|
| accrual_date |
date NOT NULL |
|
| drawn_balance |
numeric(18,2) > 0 |
snapshot at accrual time |
| daily_interest |
numeric(18,6) ≥ 0 |
6dp scale |
| interest_rate_pct |
numeric(8,5) |
snapshot rate |
| posted |
boolean |
flips true on monthly-close |
| posted_at |
timestamptz NULL |
|
| posting_id |
uuid NULL |
MOD-001 posting reference |
| trace_id, created_at, updated_at |
|
|
credit.overdraft_events (NEW; Cat 1 immutable)
Append-only lifecycle audit log.
| column |
type |
notes |
| id |
uuid PK |
|
| facility_id |
uuid NOT NULL FK |
|
| event_type |
text CHECK |
12 enum values |
| event_data |
jsonb NULL |
optional payload |
| trace_id |
uuid NOT NULL |
|
| created_at |
timestamptz |
append-only — UPDATE/DELETE blocked |
SSM outputs
| path |
value |
/bank/{stage}/credit/overdraft/create-facility-function-arn |
Lambda ARN |
/bank/{stage}/credit/overdraft/create-facility-function-name |
Lambda name |
/bank/{stage}/credit/overdraft/create-facility-api-endpoint |
Function URL |
/bank/{stage}/credit/overdraft/adjust-limit-function-arn |
|
/bank/{stage}/credit/overdraft/adjust-limit-api-endpoint |
|
/bank/{stage}/credit/overdraft/get-available-balance-function-arn |
|
/bank/{stage}/credit/overdraft/get-available-balance-api-endpoint |
|
/bank/{stage}/credit/overdraft/update-drawn-balance-function-arn |
|
/bank/{stage}/credit/overdraft/update-drawn-balance-api-endpoint |
|
/bank/{stage}/credit/overdraft/daily-accrual-sweep-function-arn |
|
/bank/{stage}/credit/overdraft/monthly-close-function-arn |
|
/bank/{stage}/credit/tables/overdraft-facilities/name |
credit.overdraft_facilities |
/bank/{stage}/credit/tables/overdraft-daily-accruals/name |
credit.overdraft_daily_accruals |
/bank/{stage}/credit/tables/overdraft-events/name |
credit.overdraft_events |
Policy satisfaction
| Policy |
Mode |
Test |
| CRE-001 |
CALC |
pol-cre-001-calc.test.ts — daily accrual + banker's rounding fixtures |
| CRE-002 |
GATE+AUTO |
pol-cre-002-gate.test.ts (negative tests) + pol-cre-002-auto.test.ts (source scan) |
| CON-005 |
GATE |
pol-con-005-gate.test.ts (negative tests on create + increase) |
| CON-008 |
ALERT |
pol-con-008-alert.test.ts — hardship + unarranged alerts fire and emit events |
| PAY-001 |
AUTO |
pol-pay-001-auto.test.ts — idempotency keys on every MOD-001 call |
Open handoffs filed at completion
MOD-117-complete.handoff.md — wiki: status In progress; wiki addendum for 7 new outbound events + AD set k-1..k-13.
MOD-117-data-model-gap.handoff.md — wiki: SD05 data model addendum for 3 new tables.
MOD-001-accrual-posting-type.handoff.md — bank-core: MOD-001 must accept posting_type='ADJUSTMENT' with references OVERDRAFT_INTEREST and OVERDRAFT_FACILITY_FEE.
MOD-003-balance-feed-consumer.handoff.md — bank-core: contract for MOD-003 real-time balance feed; replaces synchronous update-drawn-balance stub.
MOD-110-fee-waiver-stub.handoff.md — bank-credit: MOD-110 must expose a hardship-waiver checker; until then v1 returns waived=false.
MOD-117-overdraft-rate-governance-cfo-signoff.handoff.md — CFO: NZ 18.9% / AU 16.5% defaults + AppConfig override governance procedure.