Technical design — MOD-135 Batch payment & payroll file processing¶
Module: MOD-135 System: SD04 Payments Processing Repo: bank-payments FR scope: FR-601 (file upload + validation), FR-602 (aggregate balance gate), FR-603 (per-item screening + submission), FR-604 (rail-side return handling) 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-007 (AUTO), PAY-002 (LOG), CON-005 (AUTO) Author: AI coding agent (Claude) Date: 2026-05-20 Jurisdictions: AU + NZ
Objective¶
Customers upload a batch payroll file (ABA for AU, CSV for either jurisdiction). MOD-135 validates the file, gates the aggregate amount against the source account's available balance via MOD-020, asks the customer to confirm the totals (CON-005), then per-item submits each beneficiary credit through MOD-020 → MOD-001 to settle.
The chain is intentionally pipelined: the synchronous customer-facing hops (upload, confirm) stay fast; the per-item fan-out runs asynchronously in the submission Lambda, sequentially over the items (j-3 ceiling = 3 000 items / batch in v1 — fits inside the 15-min Lambda budget).
Architecture¶
Customer HTTP API (internal)
POST /internal/v1/payments/batch ─► batch-upload (FR-601)
│ mints pre-signed S3 PUT URL (j-1)
GET /internal/v1/payments/batch/{batch_id}
GET /internal/v1/payments/batch?party_id=...
POST /internal/v1/payments/batch/{batch_id}/confirm ─► batch-confirm (CON-005 / j-7)
(dev/uat only — k-1)
POST /internal/v1/payments/batch/_admin/inject-batch
POST /internal/v1/payments/batch/{batch_id}/_admin/advance-submission
POST /internal/v1/payments/batch/{batch_id}/_admin/mark-returned
S3-drop ingestion (j-1)
s3://bank-payments-batch-files-{stage}/incoming/batch/*.{aba,csv}
│ ObjectCreated:Put
▼
batch-validation.handler (FR-601 + FR-602)
│
│ 1. selectByFileKey
│ 2. mark VALIDATING + BATCH_VALIDATING audit
│ 3. fetch body from S3
│ 4. validation-orchestrator (ABA / CSV)
│ 5. MOD-020 aggregate (call_purpose=AGGREGATE)
│ INSUFFICIENT_FUNDS → shortfall_amount
│ other VALIDATION_FAILED → REJECTED
│ 6. insertPending N items (mint payment_id each)
│ 7. markPendingApproval + BATCH_VALIDATED audit
│ 8. publish bank.payments.batch_validated
Customer confirms ─► batch-confirm (CON-005 / FR-602)
│ totals must match (TOTALS_MISMATCH)
│ shortfall requires accept_partial_funding
│ mark PROCESSING + BATCH_CONFIRMED audit
│ publish bank.payments.batch_confirmed
│ fire-and-forget invoke ↓
▼
batch-submission.handler (FR-603 / FR-604 / j-3)
│
│ per item (sequential, ≤ 3000):
│ 1. ITEM_SUBMITTING + markSubmitting
│ 2. runItem(orchestrator):
│ MOD-020 BATCH_ITEM screen (FR-603 — runs MOD-013
│ sanctions + MOD-023 fraud transitively per j-4)
│ SANCTIONS_HIT / FRAUD_HIGH_RISK / PENDING_AUTH
│ → QUARANTINED + publish batch_item_quarantined
│ AUTHORISED → MOD-001 PAYMENT posting
│ 3. SETTLED → ITEM_SETTLED ; FAILED → ITEM_FAILED
│
▼ after all items:
reconciliation (FR-608 / j-5 — MOD-135-owned for v1)
parsed_total + per-status sum must equal validated_total
matched → markBatchSettled + publish batch_settled
variance → markBatchFailed + publish batch_failed
Admin (dev/uat only) — FR-604 simulation
mark-returned: MOD-001 REVERSAL (DEBIT clearing / CREDIT source)
+ markReturned + publish batch_item_returned
State machine — payments.batch_files.status:
UPLOADED → VALIDATING → PENDING_APPROVAL → PROCESSING → SETTLED | FAILED
→ REJECTED
State machine — payments.batch_items.status:
PENDING → SUBMITTING → SUBMITTED → SETTLED | QUARANTINED | FAILED | RETURNED
j-rulings applied (pre-build wiki amendments)¶
| Ruling | Implementation |
|---|---|
| j-1 S3 pre-signed PUT URL ingestion | batch-upload mints a 15-min pre-signed PUT URL. ObjectCreated:Put on the bucket triggers batch-validation. Bucket owned by MOD-135; dependsOn the lambda:Permission per the MOD-114 fix. |
| j-2 Uppercase file format enum | FileFormatEnum is "ABA" / "CSV". |
| j-3 Sequential fan-out, ~3000-item ceiling | submission-orchestrator runs sequential; ceiling enforced in validation-orchestrator (cross-cutting check) + batch-submission (runtime guard). Documented in config.ts. v2 lifts to event-fan-out / Step Functions. |
| j-4 Sanctions + fraud transitive via MOD-020 | Per-item invokeValidatePayment({ call_purpose: "BATCH_ITEM" }). MOD-020's design doc enumerates the transitive chain to MOD-013 (sanctions) + MOD-023 (fraud). MOD-135 makes no direct invocation. |
| j-5 MOD-135 owns FR-608 reconciliation in v1 | reconciliation.ts classifies per-status totals; batch-submission reconciles at end-of-batch. MOD-081 picks up the BATCH rail in v2. |
| j-6 Distinct event schemas per outbound event | 6 schemas, all draft-04, AJV-validated. event-publisher.ts schema-validates before putEvent. |
| j-7 CON-005 customer confirmation gate | batch-confirm rejects mismatched totals (TOTALS_MISMATCH), refuses shortfall without explicit accept_partial_funding (SHORTFALL_NOT_ACCEPTED). |
| j-9 payment_id minted per item before MOD-020 call | batch-validation mints a UUID per parsed item and writes it into payments.batch_items.payment_id; submission-orchestrator passes it to MOD-020 and MOD-001. |
j-10 CSV with optional item_count=N preamble |
csv-parser handles both forms; mismatch → CSV_DECLARED_COUNT_MISMATCH. |
| j-11 Caller-managed idempotency on upload | payments.batch_files.idempotency_key UNIQUE; replay returns the original upload_url + file_key. Powertools makeIdempotent wraps the upload handler so the pre-signed URL is regenerated deterministically per replay. |
| k-1 Admin gated | Three-layer (build-time skip in infra/functions.ts + infra/api.ts; runtime 404 ADMIN_ENDPOINT_DISABLED). |
SSM outputs¶
| Path | Type | Notes |
|---|---|---|
/bank/{stage}/mod-135/api/base-url |
String | API Gateway URL |
/bank/{stage}/mod-135/batch-upload-lambda/arn |
String | |
/bank/{stage}/mod-135/batch-validation-lambda/arn |
String | S3-event triggered |
/bank/{stage}/mod-135/batch-confirm-lambda/arn |
String | |
/bank/{stage}/mod-135/batch-submission-lambda/arn |
String | Async fan-out (Event-invoked) |
/bank/{stage}/mod-135/admin-lambda/arn |
String | dev/uat only (k-1) |
/bank/{stage}/mod-135/batch-files-bucket-name |
String | j-1 ingestion bucket |
/bank/{stage}/mod-135/batch-files-table |
String | payments.batch_files |
/bank/{stage}/mod-135/batch-items-table |
String | payments.batch_items |
/bank/{stage}/mod-135/batch-events-table |
String | payments.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 + REVERSAL (grant in place from MOD-119/120/122/114/136) |
/bank/{stage}/mod-020/validate-payment-lambda/arn |
MOD-020 | Same-domain aggregate (FR-602) + per-item (FR-603) screening |
/bank/{stage}/mod-135/batch-clearing-account-id |
(treasury seed; prod-only) | Batch clearing GL account |
Events¶
Published (on bank-payments bus, all 6 are distinct per j-6):
- bank.payments.batch_validated — file passed format + aggregate balance gate; awaiting customer confirmation.
- bank.payments.batch_confirmed — customer confirmed; batch transitions to PROCESSING.
- bank.payments.batch_item_quarantined — sanctions / fraud / step-up — item held for ops review (AML-007).
- bank.payments.batch_item_returned — sponsor rail returned the item; re-credit posted (FR-604).
- bank.payments.batch_settled — all items terminal, reconciliation matched.
- bank.payments.batch_failed — reconciliation variance or items_submitted = 0.
Schemas in schemas/ (draft-04, AJV-validated). MOD-135 owns the EB
Schema Registry upload.
Consumed: none directly. MOD-022 + MOD-063 catch-all rules on
bank.payments.* already cover them.
Postgres tables¶
Owned:
- payments.batch_files (V001) — batch metadata + status.
- payments.batch_items (V002) — per-line beneficiary instruction.
- payments.batch_events (V003) — append-only audit (ADR-048 Cat 1 immutability).
Soft FKs (cross-DB, no enforced FK):
- payments.batch_files.party_id / account_id — SD05 customer + SD01 account.
- payments.batch_items.payment_id — SD04 MOD-020's payments.payments (minted per item).
Idempotency:
- payments.batch_files.idempotency_key UNIQUE (caller-managed, j-11).
- payments.idempotency_records (MOD-021 V007) — Powertools store for the upload Lambda.
CloudWatch alarms¶
| Name | Trigger |
|---|---|
bank-{stage}-MOD-135-batch-upload-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-135-batch-validation-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-135-batch-confirm-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-135-batch-submission-errors |
≥ 1 Lambda Error in 3 of 5 min |
bank-{stage}-MOD-135-batch-submission-p99-latency |
p99 ≥ 12 min (j-3 budget guard) |
bank-{stage}-MOD-135-item-quarantined-rate |
≥ 20 QUARANTINED in 1 h (AML-007 signal) |
bank-{stage}-MOD-135-batch-failed-rate |
≥ 3 FAILED batches in 1 h (FR-608 escalation) |
bank-{stage}-MOD-135-shortfall-rate |
≥ 5 shortfalls in 1 h (FR-602 customer-attention) |
v1 known limitations¶
- Sequential fan-out (j-3) — ~3 000 items / batch ceiling at 150 ms /
item. v2 lifts to event-fan-out (Step Functions or SQS) for batches
3 000 items and to parallelise the per-item invokes.
- MOD-081 reconciliation deferred (j-5) — v1 reconciliation lives
inside
batch-submission; v2 lifts FR-608 into MOD-081'sV1_MATCH_RAILS = [BATCH, …]. - No real ABA writer-side hash variant probe (j-9) — accepts both the standard "last-digit-stripped" hash AND the full-10-digit variant. Real sponsor bank quirks will surface; v2 hardens to one.
- No RTGS-style high-value batch path — v1 is payroll batch only. v2 adds high-value individual instructions via a separate flow.
- Production back-office authoriser absent — the customer-facing HTTP routes are wired but presumed customer-portal authenticated upstream of the API Gateway. v2 adds an IAM/JWT authoriser.
- Hard-coded GL codes —
2100(customer-deposits) +2260(batch clearing). v2 derives fromaccounts.account_products. - Rail submission out-of-band — the actual MOD-001 CREDIT goes to the batch clearing GL; the real beneficiary settlement happens via the sponsor bank's batch-out file (not modelled in v1). v2 wires in the sponsor-out file once the rail integration is built.