MOD-008 — Dormancy & escheatment engine¶
System: SD01 Core Banking · Repo: bank-core Phase: 5 · Build status: Built (pre-deploy) Depends on: MOD-001, MOD-002, MOD-007, MOD-104, MOD-103 Date: 2026-04-30
Purpose¶
Owns the dormancy + escheatment lifecycle for SD01. Two distinct
thresholds operate on the same accounts.accounts rows:
- Operational dormancy (FR-073) — configurable, default 12 months. ACTIVE accounts with no customer-initiated transaction for ≥ N months get auto-transitioned to DORMANT via MOD-007's transition API.
- Statutory escheatment — anchored to
last_transaction_at, not todormant_at(orchestrator A1; immune to dormancy threshold reconfig): - NZ:
+12 months→ IRD, Unclaimed Money Act 1971 - AU:
+7 years→ ASIC, Banking Act 1959
At 90 / 30 / 7 days before the statutory date, MOD-008 fires
pre-escheatment notices. On the statutory date a weekly
submission cron generates a per-jurisdiction CSV file in S3 and
emits bank.core.dormancy_triggered with
dormancy_stage=ESCHEATMENT_SUBMITTED.
Reversal (FR-076) is event-driven: any customer-initiated posting on a DORMANT account triggers an immediate transition back to ACTIVE via MOD-007's HTTP API.
State data flow¶
customer activity stops
│
──── FR-073 cron at 02:00 (NZ + AU) ────
│
ACTIVE → MOD-007 transition → DORMANT
│
dormancy_records.DORMANCY_CONFIRMED
bank.core.dormancy_triggered{stage=DORMANT}
│
──── FR-074 cron at 02:15 (NZ + AU) ────
│
statutory_date - days ≤ 90 / 30 / 7
│
dormancy_records.ESCHEATMENT_NOTIFIED ×3
bank.core.dormancy_triggered{stage=ESCHEATMENT_PENDING, days_until_escheatment=N}
│
──── FR-075 cron Sun 03:00 (NZ + AU) ────
│
statutory_date ≤ today AND not yet submitted
│
CSV → s3://bank-{stage}-mod-008-escheatment-submissions/...
accounts.escheatment_submissions row (status=PENDING_OPS)
dormancy_records.ESCHEATMENT_SUBMITTED
bank.core.dormancy_triggered{stage=ESCHEATMENT_SUBMITTED}
│
ops uploads to IRD/ASIC manually (v1)
│
ops UPDATEs submission_status SUBMITTED → ACKNOWLEDGED
reversal — bank.core.posting_completed (DORMANT account, customer-initiated)
│
▼
MOD-008 reactivation-consumer → MOD-007 transition → ACTIVE
dormancy_records.REACTIVATED
Tables owned by MOD-008 (V001-V003)¶
| Table | Migration | Purpose |
|---|---|---|
accounts.dormancy_records |
V001 | Append-only audit; six event types per the SD01 wiki data model |
accounts.escheatment_submissions |
V002 | Per-(jurisdiction, period) submission file register; mutable submission_status |
| (immutability triggers) | V003 | Block UPDATE/DELETE/TRUNCATE on dormancy_records (V003 trigger; load-bearing because BankCoreRole has BYPASS RLS) |
| (V004 seed) | V004 | Two internal accounts: INTERNAL_UNCLAIMED_MONEY_NZ (NZD/NZ) and INTERNAL_UNCLAIMED_MONEY_AU (AUD/AU); is_internal=true |
The accounts.dormancy_records.idempotency_key column is a MOD-008
addition not in the wiki schema — required for the cron's natural
idempotency (re-runs derive the same key from event_type + bucket).
Cross-module integration points¶
| Module | How called | Direction |
|---|---|---|
| MOD-007 | HTTP POST /internal/v1/accounts/{id}/transition |
every transition (mark dormant, reactivate) — orchestrator A11 |
| MOD-001 | HTTP POST /internal/v1/postings |
the 2-leg escheatment journal (deferred to ops in v1; the table + bucket are wired) |
| MOD-002 | (no direct call) | MOD-002's ingest rule already widened by MOD-007 to cover account_status_changed; dormancy_triggered is a separate event we don't yet route into the immutable transaction log — flagged as follow-up |
| bank.core EventBridge bus | PutEvents for bank.core.dormancy_triggered |
publishes only; consumes bank.core.posting_completed for reactivation |
Lambdas¶
| Function | Trigger | Purpose |
|---|---|---|
Mod008DormancyDetect |
cron 02:00 NZST + 02:00 AEST | FR-073 — find ACTIVE inactive accounts and mark DORMANT |
Mod008NoticeSweep |
cron 02:15 NZST + 02:15 AEST (sequenced AFTER detect) | FR-074 — write ESCHEATMENT_NOTIFIED rows + emit dormancy_triggered events at 90/30/7 day windows |
Mod008SubmissionWriter |
cron 03:00 Sunday NZST + AEST | FR-075 — group due accounts by (jurisdiction, currency), write CSV to S3, register submission row |
Mod008ReactivationConsumer |
EB consumer of bank.core.posting_completed |
FR-076 — flip DORMANT → ACTIVE on customer-initiated posting (filter: metadata.customer_initiated != false) |
Pure scheduler logic¶
src/services/escheatment-scheduler-pure.ts is the unit-tested core:
statutoryEscheatmentDate(lastTxn, jurisdiction)— calendar arithmetic (setUTCFullYear/setUTCMonth); leap-year safe across the AU 7-year span.isDormantByThreshold(lastTxn, now, thresholdMonths)— operational dormancy gate.pendingNoticeWindows(currentDays, alreadyEmitted)— picks which 90/30/7 windows fire NOW given prior runs. Handles missed cron days correctly (e.g. cron skipped a day → both 90 and 30 due in one run).isEscheatmentDue(statutoryDate, now)— boolean for the submission cron.
Configuration¶
| Env | Default | Purpose |
|---|---|---|
DORMANCY_THRESHOLD_MONTHS |
12 | FR-073 operational threshold |
MOD007_API_BASE_URL |
(deploy-time) | resolved from /bank/{stage}/mod-007/api/base-url |
MOD001_API_BASE_URL |
(deploy-time) | resolved from /bank/{stage}/mod-001/api/base-url |
SUBMISSIONS_BUCKET |
(deploy-time) | name of the shared bank-documents-{stage} bucket (MOD-104), read from /bank/{stage}/s3/documents/name. MOD-008 writes under the mod-008/escheatment/ prefix. |
EVENTBRIDGE_BUS_ARN |
(deploy-time) | bank-core bus ARN (no runtime SSM call) |
SSM outputs¶
| Path | Type | Purpose |
|---|---|---|
/bank/{stage}/mod-008/dormancy-detect-lambda/arn |
String | Lambda ARN |
/bank/{stage}/mod-008/notice-sweep-lambda/arn |
String | Lambda ARN |
/bank/{stage}/mod-008/submission-writer-lambda/arn |
String | Lambda ARN |
/bank/{stage}/mod-008/reactivation-consumer-lambda/arn |
String | Lambda ARN |
/bank/{stage}/mod-008/escheatment-submissions-bucket |
String | S3 bucket name — points at the shared bank-documents-{stage} bucket from MOD-104; MOD-008 writes under the mod-008/escheatment/ prefix |
/bank/{stage}/mod-008/dormancy-records-table |
String | accounts.dormancy_records |
/bank/{stage}/mod-008/escheatment-submissions-table |
String | accounts.escheatment_submissions |
/bank/{stage}/mod-008/dormancy-threshold-months |
String | 12 |
Policy mapping¶
| Policy | Mode | How satisfied | Test |
|---|---|---|---|
| CON-001 | AUTO | 90/30/7-day pre-escheatment notice records written to dormancy_records before the ESCHEATMENT_SUBMITTED row; events published with dormancy_stage=ESCHEATMENT_PENDING so the (future) notification orchestration module can deliver. |
tests/policy/con-001-auto-pre-escheatment-notice.test.ts |
| REP-001 | AUTO | escheatment_submissions row carries jurisdiction, period_end, regulator (IRD/ASIC), account_count, total_amount, s3_key, created_at, submission_status lifecycle. CHECK constraint enforces regulator ∈ {IRD, ASIC}. |
tests/policy/rep-001-auto-regulator-submission.test.ts |
Known limitations / follow-ups¶
- Customer activity definition — currently relies on
accounts.accounts.last_transaction_at, which MOD-001 updates synchronously on every posting (no distinction between system and customer postings). Once MOD-005 (accruals) and MOD-110 (fees) ship they'll need to flag system-generated postings viametadata.customer_initiated=false. The reactivation consumer already filters on this flag (orchestrator A8 — preemptive) so the reversal path is future-proof. The dormancy-detect query needs follow-up at that point. - Notice delivery — FR-074 says "delivered via the notification orchestration module". No such module exists. MOD-008 emits the event + writes the audit row; the actual delivery (email/SMS) is deferred. The consumer module is MOD-063 per the wiki event catalogue.
- Escheatment posting + close — MOD-008 v1 generates the submission file but does NOT yet post the 2-leg debit/credit journal or close the account. Ops uploads the file to IRD/ASIC manually; a future iteration drives the posting + close once the regulator acknowledgement returns. The internal unclaimed-money accounts (V004 seed) are ready to receive the credits.
- Holds blocked from escheatment — accounts with active
pending_holds carry
HOLD_BLOCKED=TRUEin the submission CSV (hold_blockedcolumn). Ops handles manually until a future iteration coordinates hold release with the escheatment posting. dormancy_triggeredingest into MOD-002 — not currently in MOD-002's ingest rule pattern. Adding it would put status events- dormancy events together in
core.transaction_log. 1-line pattern widen, deferred until MOD-002's ingest handler tolerates events withoutposting_id/amount(also flagged in MOD-007's handoff).