Skip to content

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

  1. MOD-117-complete.handoff.md — wiki: status In progress; wiki addendum for 7 new outbound events + AD set k-1..k-13.
  2. MOD-117-data-model-gap.handoff.md — wiki: SD05 data model addendum for 3 new tables.
  3. MOD-001-accrual-posting-type.handoff.md — bank-core: MOD-001 must accept posting_type='ADJUSTMENT' with references OVERDRAFT_INTEREST and OVERDRAFT_FACILITY_FEE.
  4. MOD-003-balance-feed-consumer.handoff.md — bank-core: contract for MOD-003 real-time balance feed; replaces synchronous update-drawn-balance stub.
  5. MOD-110-fee-waiver-stub.handoff.md — bank-credit: MOD-110 must expose a hardship-waiver checker; until then v1 returns waived=false.
  6. MOD-117-overdraft-rate-governance-cfo-signoff.handoff.md — CFO: NZ 18.9% / AU 16.5% defaults + AppConfig override governance procedure.