Technical design — MOD-137 Agency banking adapter¶
Module: MOD-137 System: SD04 Payments Processing Repo: bank-payments FR scope: FR-609 (file ingestion + account matching + quarantine), FR-610 (posting + customer notification), FR-611 (AML threshold flag), FR-612 (settlement reconciliation) NFR scope: NFR-012 (Postgres write latency p99 ≤ 10 ms), NFR-019 (RTO/RPO Tier 1), NFR-024 (audit immutability) Policies satisfied: PAY-001 (GATE), AML-005 (AUTO), CON-001 (AUTO), PAY-002 (LOG) Author: AI coding agent (Claude) Date: 2026-05-20 Jurisdictions: AU + NZ
Objective¶
Agency banking lets customers perform cash transactions (deposits, withdrawals, balance enquiries) at third-party outlets — Australia Post primary, NZ Post and "other" networks configurable. Integration is file-based: the agency network drops daily batch files into S3 (operationally via SFTP-gateway upstream of S3); MOD-137 parses, matches each row to a live customer account, validates account status + available balance for withdrawals, posts via MOD-001, flags threshold-crossing cash transactions for AML reporting (FR-611), and reconciles the processed batch against the network's settlement file (FR-612).
Architecture¶
S3-drop ingestion (k-1 / k-6 — single bucket, two prefixes)
s3://bank-payments-agency-files-{stage}/
agency-files/transactions/{NETWORK}/{batch-date}/{file_ref}.csv ─► batch-ingest
agency-files/settlements/{NETWORK}/{batch-date}/{file_ref}.settlement.csv ─► settlement-ingest
batch-ingest (k-9 idempotent on sha256(bucket:key))
│ 1. parseAgencyFile (network-driven router; balance-enquiry filtered at parse)
│ 2. insertReceived (file_reference UNIQUE — file-level idempotency)
│ 3. insertUnmatched per parsed item
│ 4. BATCH_RECEIVED audit + publish agency_batch_received
│ 5. fire-and-forget invoke batch-processing
▼
batch-processing (async fan-out)
│ per item:
│ 1. account-matcher.findAccount (k-2 — cross-schema SELECT on core.accounts)
│ 2. validateItem (FR-609 — account-active + MOD-003 balance for withdrawals)
│ → QUARANTINE → markQuarantined + TRANSACTION_QUARANTINED audit + publish
│ agency_item_quarantined
│ → PASS continues
│ 3. detectAmlThreshold (FR-611 — runs alongside posting, never gates it)
│ 4. MOD-001 PAYMENT posting:
│ DEPOSIT → CREDIT customer / DEBIT clearing GL (2270)
│ WITHDRAWAL → DEBIT customer / CREDIT clearing GL
│ 5. markPosted + TRANSACTION_POSTED audit
│ 6. if AML flagged → TRANSACTION_AML_FLAGGED audit + publish
│ agency_aml_threshold_flagged
│ after all items:
│ 7. markSettled + BATCH_SETTLED audit + publish agency_batch_settled
▼
settlement-ingest (FR-612 / k-3 — MOD-137 v1 owner)
│ 1. parseSettlement (small CSV: file_reference + per-type counts + totals)
│ 2. selectByFileReference
│ 3. totalsForBatch (per-type processed totals from agency_transactions)
│ 4. reconcile() matched → markReconciled + BATCH_RECONCILED audit
│ mismatch → markExceptions + BATCH_EXCEPTIONS audit +
│ publish agency_reconciliation_discrepancy
Customer HTTP API (read-only)
GET /internal/v1/payments/agency/batch/{batch_id}
GET /internal/v1/payments/agency/batch?limit=N
Admin (dev/uat only — k-1)
POST /internal/v1/payments/agency/_admin/inject-batch
POST /internal/v1/payments/agency/batch/{batch_id}/_admin/advance-processing
POST /internal/v1/payments/agency/batch/{batch_id}/_admin/inject-settlement
State machine — payments.agency_batch_files.status:
RECEIVED → PROCESSING → SETTLED → RECONCILED | EXCEPTIONS
State machine — payments.agency_transactions.status:
UNMATCHED → MATCHED → POSTED | QUARANTINED
k-rulings applied (pre-build wiki amendments)¶
| Ruling | Implementation |
|---|---|
| k-1 S3-drop ingestion (no SFTP gateway in v1) | bank-payments-agency-files-{stage} bucket owned by MOD-137; ObjectCreated:Put on agency-files/transactions/ triggers batch-ingest; SFTP gateway sits operationally upstream and is out of MOD-137's scope. |
| k-2 Direct Postgres SELECT on core.accounts | account-matcher.ts queries core.accounts(id, bsb, account_number, status) via the same Neon pool — soft FK, no Lambda hop, no cross-domain IAM grant. Cross-schema read grant required for bank_payments_app_user. |
| k-3 MOD-137 owns FR-612 in v1 | reconciliation.ts pure module classifies per-type counts + totals; settlement-ingest runs it after batch SETTLED. MOD-081 adds AGENCY to V1_MATCH_RAILS in v2. MOD-081 marked optional in MOD-137.yaml. |
| k-4 EB event for AML reporting | bank.payments.agency_aml_threshold_flagged carries terminal_id + agent_outlet_code (FR-611 required location metadata). Consumed by SD03 MOD-016 (typology engine) + SD06 MOD-037 (regulatory reporting via CDC). Not ifti_cmir_queue — that's the cross-border surface. |
| k-5 SSM-driven AML thresholds | /bank/{stage}/mod-137/aml/deposit-threshold-{aud,nzd} per stage. Loaded at cold start, env-injected. Default 10000.00 in dev/uat; treasury seeds prod via SSM. Operator-adjustable without code change. |
| k-6 Two S3 prefixes + two Lambdas | Single bucket, two distinct lambdaFunctions entries on the BucketNotification (transactions/ → batch-ingest, settlements/ → settlement-ingest). dependsOn the lambda:Permission per the MOD-114/135/136 race-fix. |
| k-7 Per-transaction notification | FR-610 verbatim; MOD-063 catch-all on bank.payments.* consumes agency_batch_settled. Agency volumes are modest — no fan-out concerns. |
| k-8 Wiki amendments delivered | bank-wiki commit 6770ad39 added the 3 tables + 5 events to SD04 data model + event catalogue before this build. |
| k-9 Dual idempotency | (a) file_reference UNIQUE on payments.agency_batch_files. (b) Powertools makeIdempotent on the S3 handler keyed on sha256(bucket:key) via payments.idempotency_records. |
| k-10 Uppercase enums | agency_network, transaction_type, status, event_type, entity_type all UPPERCASE per SD04 convention. |
| k-11 MOD-103 + MOD-104 deps | Added to MOD-137.yaml dependencies as optional: false (platform foundations). |
SSM outputs¶
| Path | Type | Notes |
|---|---|---|
/bank/{stage}/mod-137/api/base-url |
String | API Gateway URL (read-only routes + admin in dev/uat) |
/bank/{stage}/mod-137/batch-ingest-lambda/arn |
String | S3-event triggered |
/bank/{stage}/mod-137/batch-processing-lambda/arn |
String | Async fan-out |
/bank/{stage}/mod-137/settlement-ingest-lambda/arn |
String | S3-event triggered |
/bank/{stage}/mod-137/batch-status-lambda/arn |
String | HTTP read-only |
/bank/{stage}/mod-137/admin-lambda/arn |
String | dev/uat only (k-1) |
/bank/{stage}/mod-137/agency-files-bucket-name |
String | k-1 / k-6 ingestion bucket |
/bank/{stage}/mod-137/agency-batch-files-table |
String | payments.agency_batch_files |
/bank/{stage}/mod-137/agency-transactions-table |
String | payments.agency_transactions |
/bank/{stage}/mod-137/agency-batch-events-table |
String | payments.agency_batch_events (immutable) |
Upstream SSM consumed¶
| Path | Source | Use |
|---|---|---|
/bank/{stage}/iam/lambda/bank-payments/arn |
MOD-104 | Role |
/bank/{stage}/observability/adot-nodejs-layer-arn |
MOD-076 | OTel |
/bank/{stage}/sns/alerts/arn |
MOD-104 | Alarms |
/bank/{stage}/eventbridge/bank-payments/arn |
MOD-104 | Publish bus |
/bank/{stage}/neon/pooler-host + bank-neon/{stage}/payments_app_user |
MOD-103 | DB |
/bank/{stage}/mod-001/lambda/arn |
MOD-001 | Cross-domain MOD-001 PAYMENT (grant in place from MOD-119/120/122/114/135/136) |
/bank/{stage}/mod-003/balance-query-lambda/arn |
MOD-003 | Per-withdrawal balance gate (FR-609) |
/bank/{stage}/mod-137/aml/deposit-threshold-{aud,nzd} |
(treasury seed; prod-only) | k-5 |
/bank/{stage}/mod-137/agency-clearing-account-id |
(treasury seed; prod-only) | Agency clearing GL account |
Events¶
Published (on bank-payments bus, all 5 distinct per k-4):
- bank.payments.agency_batch_received — file landed + parsed; per-item processing queued.
- bank.payments.agency_batch_settled — all items terminal.
- bank.payments.agency_item_quarantined — account-not-found / inactive / insufficient-balance / posting-failed (FR-609).
- bank.payments.agency_aml_threshold_flagged — AML-005 / FR-611. Carries terminal_id + agent_outlet_code. Consumed by SD03 MOD-016 + SD06 MOD-037.
- bank.payments.agency_reconciliation_discrepancy — FR-612 mismatch flagged to ops.
Schemas in schemas/ (draft-04, AJV-validated).
Consumed: none directly. MOD-022 + MOD-063 catch-all rules on
bank.payments.* already cover all 5 events.
Postgres tables¶
Owned:
- payments.agency_batch_files (V001) — mutable batch metadata.
- payments.agency_transactions (V002) — per-line transaction rows.
- payments.agency_batch_events (V003) — append-only audit (ADR-048 Cat 1 immutability).
Soft FKs (cross-DB / cross-schema, no enforced FK):
- payments.agency_transactions.account_id → SD01 core.accounts(id) (k-2 cross-schema read).
- payments.agency_transactions.posting_id → SD01 accounts.postings(id).
Idempotency:
- payments.agency_batch_files.file_reference UNIQUE (k-9 file-level).
- payments.idempotency_records (MOD-021 V007) — Powertools store for the S3 handler (sha256(bucket:key)).
CloudWatch alarms¶
| Name | Trigger |
|---|---|
bank-{stage}-MOD-137-batch-ingest-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-137-batch-processing-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-137-settlement-ingest-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-137-batch-status-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-137-batch-processing-p99-latency |
p99 ≥ 12 min (Lambda budget guard) |
bank-{stage}-MOD-137-quarantine-rate |
≥ 20 QUARANTINED in 1 h (FR-609 quality signal) |
bank-{stage}-MOD-137-recon-discrepancy-rate |
≥ 3 discrepancies in 2 h (FR-612 escalation) |
bank-{stage}-MOD-137-aml-flag-rate |
≥ 50 AML flags in 1 h (visibility — adjust to baseline) |
v1 known limitations¶
- SFTP gateway is out of scope (k-1) — v1 assumes files land directly in S3. v2 may add an SFTP→S3 gateway as a separate module (or operational concern).
- MOD-081 reconciliation deferred (k-3) — v1 reconciliation lives inside
settlement-ingest. v2 lifts FR-612 into MOD-081'sV1_MATCH_RAILS = [..., AGENCY]. - Single CSV envelope for AU + NZ + OTHER — v2 splits parsers if the formats diverge.
k-2 leaves
OTHERfalling back to the AU envelope with caller-supplied currency. - Account-matching by exact (BSB, account_number) — no fuzzy match in v1. Unmatched items quarantine for ops to manually resolve.
- Balance enquiries are dropped at parse — the SD04 schema CHECK
(amount > 0) on
agency_transactionswould forbid storing them. Counted on the batch row + logged via parse-summary, not modelled as rows. v2 may add anagency_balance_enquiriesaudit-only table. - Hard-coded GL codes —
2100(customer-deposits) +2270(agency clearing). v2 derives fromaccounts.account_productsper-account. - Customer-portal authoriser absent — read-only routes are wired but presumed back-office-network-restricted. v2 adds an IAM/JWT authoriser.
- AML threshold cents-only — the threshold values are loaded as fixed-point decimals. Currency-pair-aware (AUD vs NZD) but no exchange-rate normalisation — i.e. an AUD 9999 cross-border transfer wouldn't be flagged. Out of scope for agency banking (always domestic).