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:
- Computes the deterministic outcome at request time and persists
the full record to the
bank-platform-stub-state-{stage}DDB table. - Sends an SQS
SendMessagewithDelaySecondsequal to the provider's configured delay (e.g. 2s for Onfido, 3s for NPP). - Returns
200to the consumer withaccepted: true.
The async-callback-firer Lambda (SQS consumer):
- Reads the DDB record (full payload + outcome).
- Resolves the consumer's callback URL from
/<repo>/<stage>/<provider>/callback-url. Consumer modules write this at their own deploy time. - POSTs the outcome to the callback URL.
- 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 instages-prod-skip.test.ts; structurally enforced by the per-module workflow only listingdevanduatin 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.tsmust 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 insrc/config/ssm-paths.ts(consumer-facing keys) + (optional) a new pattern function insrc/shared/pattern-match.ts+ a route handler branch insrc/lambdas/stub-router/index.ts.