Skip to content

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:

  1. 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.
  2. Statutory escheatment — anchored to last_transaction_at, not to dormant_at (orchestrator A1; immune to dormancy threshold reconfig):
  3. NZ: +12 months → IRD, Unclaimed Money Act 1971
  4. 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 via metadata.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=TRUE in the submission CSV (hold_blocked column). Ops handles manually until a future iteration coordinates hold release with the escheatment posting.
  • dormancy_triggered ingest 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 without posting_id / amount (also flagged in MOD-007's handoff).