Skip to content

MOD-157 — External provider stub service

Purpose

MOD-157 deploys deterministic Lambda stubs for every external third-party API used across the eight system domains, plus notification-capture infrastructure for Pinpoint and a rules-based fraud-model stub for MOD-023. Without it, integration tests either call real production endpoints (unsafe, expensive, non-deterministic) or each module reinvents mocking inconsistently.

FR scope: the module spec carries no FR list (requirements: []) — this is a tooling module, not a compliance-bearing one. Tests map to the spec's documented behaviour, not to FR-NNN identifiers.

Architectural decisions: ADR-044 covers the stub strategy, ADR-045 the broader test-environment fidelity model.

Architecture

consuming module Lambda (e.g. MOD-009 eIDV)
  ├── reads SSM /<repo>/<stage>/<provider>/base-url (cold start)
  └── HTTPS POST → API Gateway HTTP API
                    └─→ stub-router Lambda (single Lambda, dispatches
                        by URL path)
                          ├── sync categories: return outcome inline
                          │   (sanctions / bureau / open banking)
                          └── async categories: write DDB record,
                              enqueue SQS DelayMessage, return 200
                                └─→ async-callback-firer Lambda
                                      ├── reads consumer callback URL
                                      │   from /<repo>/<stage>/<provider>/callback-url
                                      ├── POSTs deterministic outcome
                                      └── marks DDB record fired/failed

Deployment scope

dev and uat ONLY. sst.config.ts short-circuits when stage === 'prod' — running sst deploy --stage prod against this module is a no-op by design. The single source of truth is src/config/stages.ts (STUB_DEPLOY_STAGES = ['dev', 'uat']). A unit test (__tests__/unit/stages-prod-skip.test.ts) pins the contract.

Stage What happens
dev All resources provisioned; consumer SSM paths point at the dev API Gateway
uat All resources provisioned; consumer SSM paths point at the uat API Gateway
prod No resources. Consumer SSM paths point at real provider URLs (written by the consuming module)

Test pattern convention (ADR-044 §3)

Stub responses are driven by patterns in the request input, not by configuration switches. Pattern functions are in src/shared/pattern-match.ts (pure, unit-tested).

Provider Test input Outcome
eIDV (all) document_ref: "PASS-*" verified
eIDV (all) document_ref: "FAIL-*" rejected
eIDV (all) document_ref: "REFER-*" refer
eIDV (all) document_ref: "SANCTION-*" sanction-hit
Sanctions name contains SANCTIONED match-confirmed
Sanctions name contains PEP- pep-only
NPP destination_account ends 0001 cleared
NPP destination_account ends 0002 dishonoured
NPP destination_account ends 0003 timeout
BECS payer_bsb: "062-000" all-honour
BECS payer_bsb: "062-001" second-presentment-dishonours
BECS payer_bsb: "062-002" all-dishonour
Bureau date_of_birth: "1900-01-01" no-record
Bureau date_of_birth: "1900-01-02" adverse-record
Fraud model amount > 10000 or ref contains FRAUD-TEST 0.9

Async stub flow (ADR-044 §4)

For Onfido / NPP / BECS / SWIFT / ESAS / NZFP, the stub:

  1. Computes the deterministic outcome at request time and persists the full record to the bank-platform-stub-state-{stage} DDB table.
  2. Sends an SQS SendMessage with DelaySeconds equal to the provider's configured delay (e.g. 2s for Onfido, 3s for NPP).
  3. Returns 200 to the consumer with accepted: true.

The async-callback-firer Lambda (SQS consumer):

  1. Reads the DDB record (full payload + outcome).
  2. Resolves the consumer's callback URL from /<repo>/<stage>/<provider>/callback-url. Consumer modules write this at their own deploy time.
  3. POSTs the outcome to the callback URL.
  4. Updates the DDB record to fired / failed.

If the callback URL is missing → SQS redrive 3× → DLQ. The stub record stays pending and tests can assert the state.

Notification capture (ADR-044 §5)

MOD-063 publishes every dispatched notification to an SNS topic. In dev/uat this topic is bank-platform-notification-capture-{stage}, declared in src/stacks/notification-capture.ts. The capture Lambda subscribes and writes each message to a DynamoDB table. Integration tests query the table to assert delivery.

Schema: | Field | Type | | |---|---|---| | customer_id | S | PK | | dispatched_at | S (ISO 8601) | SK | | notification_id, type, recipient, subject, body | S | | | ttl_epoch | N | auto-expire after 30d |

Fraud model stub (ADR-044 §6)

MOD-023's inference Lambda reads its model artefact path from /bank-payments/{stage}/fraud/model-s3-path. In dev/uat this points to a JSON descriptor MOD-157 deploys to MOD-104's existing bank-artefacts-{stage} bucket (no new bucket; avoids the GOV-005 SCP s3:CreateBucket issue). The descriptor declares the rules-based scoring; MOD-023 applies it identically to a real XGBoost model.

In prod, MOD-023 writes its own production-model SSM path; MOD-157 isn't deployed.

SSM outputs

Consumer-facing (the FR-734 contract)

Pattern: /{consumer-repo}/{stage}/{provider}/base-url — same key shape across stages, value differs:

SSM path Value Consumed by
/bank-kyc/{stage}/eidv/{dvs,dia,onfido,equifax,centrix}/base-url API Gateway base URL (dev/uat) or real provider URL (prod) MOD-009 eIDV handlers
/bank-kyc/{stage}/sanctions/base-url Same MOD-013 sanctions screener
/bank-kyc/{stage}/bureau/{equifax,centrix}/base-url Same MOD-010 bureau enquiry
/bank-payments/{stage}/clearing/{npp,becs,swift,bpay,esas,nzfp}/base-url Same SD04 payment modules
/bank-payments/{stage}/openbanking/{akahu,cdr}/base-url Same Open banking adapters
/bank-payments/{stage}/post-sftp/base-url Same Agency banking
/bank-payments/{stage}/fraud/model-s3-path s3:// URI (stub JSON in dev/uat, real model in prod) MOD-023 fraud inference
/bank-platform/{stage}/notifications/capture/api-url Notification capture API MOD-160 acceptance scenarios

Source of truth: src/config/ssm-paths.ts (one entry per key). Adding a path is a one-line change there.

Module-internal (operational + integration tests)

Pattern: /bank/{stage}/mod157/...

SSM path Value Consumed by
/bank/{stage}/mod157/api/base-url API Gateway base URL Integration tests
/bank/{stage}/mod157/api/id API Gateway ID Dashboards
/bank/{stage}/mod157/state-table/{name,arn} DDB stub-state table Tests, async firer
/bank/{stage}/mod157/async-queue/{url,arn} SQS callback queue Tests
/bank/{stage}/mod157/async-dlq/arn DLQ ARN MOD-076 alarms
/bank/{stage}/mod157/stub-router/arn Lambda ARN Ops
/bank/{stage}/mod157/async-firer/arn Lambda ARN Ops
/bank/{stage}/mod157/notification-capture/{fn-arn,table,topic-arn} Notification capture MOD-063 (publishes to topic), tests
/bank/{stage}/mod157/fraud-model/s3-path Stub model artefact path Audit

Dependencies

  • MOD-104 — AWS account, OIDC trust, bank-artefacts-{stage} bucket (consumed for fraud model stub).
  • MOD-076 — observability layer (ADOT) attached to all stub Lambdas.
  • MOD-045 — Secrets Manager (used by MOD-157 only if the notification-capture flow needs a signing secret; not required today).

Constraints

  • No prod deploy. The stage === 'prod' short-circuit is the load-bearing safety property. Unit-tested in stages-prod-skip.test.ts; structurally enforced by the per-module workflow only listing dev and uat in the dispatch options.
  • Public API. API Gateway has auth: NONE. This is intentional — ease of integration testing without auth complexity. The whole module only exists in non-prod.
  • Pattern-match purity. Every category's outcome function in src/shared/pattern-match.ts must remain pure (no side effects, no network calls) — they're the contract integration tests rely on.
  • Adding a provider: one entry in src/config/providers.ts (route + delay) + one or more entries in src/config/ssm-paths.ts (consumer-facing keys) + (optional) a new pattern function in src/shared/pattern-match.ts + a route handler branch in src/lambdas/stub-router/index.ts.