Skip to content

Technical design — MOD-136 BPAY biller registration & inbound BPAY

Module: MOD-136 System: SD04 Payments Processing Repo: bank-payments FR scope: FR-605, FR-606, FR-607, FR-608 NFR scope: NFR-012 (Postgres write latency p99 ≤ 10 ms), NFR-019 (RTO/RPO Tier 1), NFR-024 (audit immutability) Policies satisfied: PAY-001 (AUTO), PAY-002 (LOG), REP-005 (LOG), CON-005 (AUTO — j-1 ruling) Author: AI coding agent (Claude) Date: 2026-05-19 Jurisdiction: Australia only

Objective

The complement to MOD-119: where MOD-119 handles outbound BPAY (our customers paying bills), MOD-136 handles inbound BPAY (our business-banking customers receiving bill payments registered as BPAY billers via the sponsor bank).

The flow is back-office driven: ops register a business customer as a biller via the sponsor → sponsor returns a BPAY code (typically 3–5 business days) → biller status flips to ACTIVE. Settlement files from the sponsor arrive daily on S3 (j-6 mirror of MOD-114) → per row: CRN validation → MOD-001 credit posting → MOD-063 notification within 60 s (FR-607). MOD-081 reconciliation deferred to v2 per j-5; v1 reconciliation is owned inside MOD-136.

Architecture

Back-office HTTP API (internal)
  POST   /internal/v1/payments/bpay-inbound/billers                       ─► biller-registration  (FR-605)
  GET    /internal/v1/payments/bpay-inbound/billers?party_id={uuid}
  GET    /internal/v1/payments/bpay-inbound/billers/{biller_id}
  PATCH  /internal/v1/payments/bpay-inbound/billers/{biller_id}/activate
  PATCH  /internal/v1/payments/bpay-inbound/billers/{biller_id}/status
  (dev/uat only — k-1, j-10)
  POST   /internal/v1/payments/bpay-inbound/_admin/inject-settlement-file
  POST   /internal/v1/payments/bpay-inbound/_admin/billers/{biller_id}/advance

S3-drop ingestion (j-6)
  s3://bank-payments-bpay-inbound-files-{stage}/incoming/bpay/*.json
                            │ ObjectCreated:Put
                  settlement-ingest.handler
                            │ per row (inbound-processor):
                            │   1. selectByBpayCode  — find the biller
                            │   2. INSERT bpay_inbound_payments at RECEIVED
                            │   3. FR-605 biller-status gate
                            │      → PENDING|SUSPENDED|CANCELLED → RETURNED
                            │   4. FR-606 CRN validation
                            │      → fail → CRN_INVALID + RETURNED
                            │   5. j-3 INSERT synthetic payments.payments
                            │   6. MOD-001 credit (DEBIT clearing / CREDIT biller)
                            │   7. Mark POSTED + publish bpay_inbound_received
                            │      (or POSTING_FAILED + RETURNED + bpay_inbound_returned)
                            ▼ after all rows:
                  FR-608 reconciliation (j-5 — MOD-136-owned for v1)
                  per-row reconciliation_status MATCHED|UNMATCHED
                  + INBOUND_RECONCILED batch audit row

State machine — payments.bpay_inbound_billers.status (j-2 table name):
  PENDING_REGISTRATION → ACTIVE   (FR-605 — sponsor confirmed code; j-10 staff PATCH or dev admin advance)
                       → CANCELLED|SUSPENDED  (manual ops)

State machine — payments.bpay_inbound_payments.status:
  RECEIVED → CRN_VALID → POSTED       (happy path)
                       → POSTING_FAILED → RETURNED
           → CRN_INVALID → RETURNED

j-rulings applied (pre-build wiki amendments)

Ruling Implementation
j-1 CON-005 (not CON-001) Wiki MOD-136.yaml retagged. con-005-auto.test.ts asserts the publish wiring + no-suppress tokens.
j-2 bpay_inbound_billers (not bpay_billers) Distinct from MOD-119's bpay_biller_cache (outbound directory).
j-3 Synthetic payments.payments row payments-row-writer.ts inserts one row per inbound credit with direction='INBOUND', payment_type='BPAY'. MOD-022's audit-consumer keys SETTLEMENT_CONFIRMED on this row when bank.core.posting_completed fires.
j-4 Three new EB events bpay_biller_registered / bpay_inbound_received / bpay_inbound_returned. AJV-validated; MOD-136 owns the EB Schema Registry upload.
j-5 MOD-136 owns FR-608 recon in v1 reconciliation.ts pure module classifies per-row + per-batch outcomes; settlement-ingest calls it after the per-row loop. v2 lifts to MOD-081.
j-6 S3-drop ingestion bank-payments-bpay-inbound-files-{stage} bucket owned by MOD-136; ObjectCreated:Put → settlement-ingest Lambda. dependsOn the lambda:Permission per the MOD-114 fix.
j-7 CRN-validator copied from MOD-119 crn-validator.ts is a verbatim copy. v2 cleanup: shared @bank-payments/bpay-crn package.
j-8 Uppercase CRN enum LUHN / REGEX / FIXED_LENGTH / NONE (matches MOD-119 + SD04 standard).
j-9 dev/uat admin pattern (k-1) Build-time skip in infra/functions.ts + infra/api.ts; runtime 404 ADMIN_ENDPOINT_DISABLED. v2 staff-authed (IAM or JWT) prod endpoint.
j-10 Activation flow Prod = staff PATCH /activate once sponsor confirms; dev/uat = admin /_admin/billers/{id}/advance (auto-generates bpay_code + sponsor_confirmation_ref if not supplied).
k-1 Admin gated Three-layer (build-time skip + api.ts skip + runtime 404).
k-5 Clearing account Per-environment SSM in prod; placeholder 0...000000136 in dev/uat.

SSM outputs

Path Type Notes
/bank/{stage}/mod-136/api/base-url String API Gateway URL
/bank/{stage}/mod-136/biller-registration-lambda/arn String
/bank/{stage}/mod-136/settlement-ingest-lambda/arn String
/bank/{stage}/mod-136/admin-lambda/arn String dev/uat only (k-1)
/bank/{stage}/mod-136/bpay-inbound-files-bucket-name String j-6 ingestion bucket
/bank/{stage}/mod-136/bpay-inbound-billers-table String payments.bpay_inbound_billers
/bank/{stage}/mod-136/bpay-inbound-payments-table String payments.bpay_inbound_payments
/bank/{stage}/mod-136/bpay-inbound-events-table String payments.bpay_inbound_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 credit posting (grant in place from MOD-119/120/122/114)
/bank/{stage}/mod-136/bpay-inbound-clearing-account-id (treasury seed; prod-only) BPAY inbound clearing GL

Events

Published (on bank-payments bus): - bank.payments.bpay_biller_registered — biller status → ACTIVE. - bank.payments.bpay_inbound_received — credit posted (FR-607). - bank.payments.bpay_inbound_returned — quarantined (FR-606 / FR-605 / posting failure).

Schemas in schemas/ (draft-04, AJV-validated). MOD-136 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.bpay_inbound_billers (V001) — registration registry. - payments.bpay_inbound_payments (V002) — per-row settlement ledger. - payments.bpay_inbound_events (V003) — append-only audit (ADR-048 Cat 1 immutability trigger).

Written cross-table: - payments.payments — synthetic row per inbound credit (j-3, payment_type='BPAY', direction='INBOUND'). - payments.idempotency_records (MOD-021 V007) — Powertools store.

CloudWatch alarms

Name Trigger
bank-{stage}-MOD-136-biller-registration-errors ≥ 1 Lambda Error in 3 of 5 min
bank-{stage}-MOD-136-settlement-ingest-errors ≥ 1 Lambda Error in 3 of 5 min
bank-{stage}-MOD-136-settlement-ingest-p99-latency p99 ≥ 30 s
bank-{stage}-MOD-136-inbound-returned-rate ≥ 20 RETURNED in 1 h (FR-606 signal)
bank-{stage}-MOD-136-recon-unmatched-rate ≥ 5 unmatched in 2 h (FR-608 escalation)

v1 known limitations

  • Real BPAY inbound file format — v1 accepts a JSON envelope. v2 swaps the file-parser to the sponsor's real format.
  • MOD-081 reconciliation deferred (j-5) — v2 lifts FR-608 into MOD-081's V1_MATCH_RAILS = [BPAY-inbound, …].
  • Production back-office HTTP authoriser — v1 has no IAM/JWT staff authoriser; the biller-registration endpoints are wired but presumed back-office-network-restricted. v2 adds an IAM authoriser.
  • Hard-coded GL codes2100 (customer-deposits) + 2250 (BPAY inbound clearing). v2 derives from accounts.account_products.
  • CRN-validator code duplication (j-7) — copied from MOD-119 verbatim. v2 lifts to a shared @bank-payments/bpay-crn package.
  • Reconciliation escalation — v1 marks unmatched + audits the batch; ops review unmatched rows manually. v2 builds an automated escalation that flips reconciliation_status → ESCALATED after the FR-608 "next business day" window expires.