Skip to content

MOD-128 — Credit bureau enquiry & CCR integration (technical design)

Module: MOD-128 — Credit bureau enquiry and CCR integration System: SD05 — Credit Decisioning & Loan Platform Repo: bank-credit Type: Application Lambda + IaC + Flyway Status (at build): In progress → Built (CI-driven advance) FR scope: FR-577, FR-578, FR-579, FR-580 Policies satisfied: CRE-003 (GATE), PRI-001 (GATE), CON-004 (LOG), REP-010 (LOG) ADRs in effect: ADR-001 (Postgres OLTP), ADR-007 (Lambda compute), ADR-022 (SERIALIZABLE), ADR-023 (PgBouncer pooling), ADR-031 (observability), ADR-038 (Tier 1 hot path), ADR-042 (jurisdiction runtime), ADR-043 (repo+module structure), ADR-044 (TypeScript), ADR-045 (Vitest), ADR-048 (DB-enforced invariants), ADR-051 (EventBridge bus naming), ADR-052 (Neon db naming), ADR-053 (build artefact promotion).


1. Purpose

MOD-128 is the single integration point for inbound credit bureau enquiries by SD05. SD05 credit decisioning callers (MOD-029 once built — also future MOD-027 affordability and MOD-059 outbound CCR submissions) invoke MOD-128 to retrieve a credit file/score for an applicant. MOD-128 enforces consent, identity, and duplicate-suppression gates, routes to the correct bureau by jurisdiction, normalises the response, and persists an immutable enquiry record in credit.bureau_enquiries.

Consumer-facing contract is a Function URL (IAM-auth) returning a structured (enquiry_id, bureau_name, credit_score, adverse_flags, requires_disclosure, suppressed, primary_bureau_failed, fallback_bureau_used, responded_at, trace_id) shape. Adverse-action disclosure (MOD-050) is the caller's responsibility per the orchestrator's CON-004 LOG ruling — MOD-128 surfaces adverse flags + bureau name + a requires_disclosure flag; MOD-029 routes to MOD-050.


2. Architecture

caller (MOD-029 etc.)
       │  POST {Function URL}  (SigV4-signed, BankCreditRole-bearing)
   MOD-128 Lambda (arm64, ADOT layer, BankCreditRole)
       ├── Identity gate ────► AP-010 pattern 1: SELECT from
       │                       banking.customer_relationships_identity_readable
       │                       (bank_kyc_readonly, direct host)
       ├── Consent gate ─────► SELECT from credit.bureau_consents
       │                       (stand-in for MOD-049, V001 migration)
       ├── Suppression ──────► SELECT from credit.bureau_enquiries
       │                       (last 30 days, suppressed=false, success)
       ├── Bureau routing ───► SELECT from credit.bureau_config
       │                       (jurisdiction, product_type, enquiry_purpose)
       ├── Bureau call ──────► HTTP → primary bureau (Equifax AU / Centrix / …)
       │                       fallback to illion on no_file/timeout (FR-579)
       └── Persist ──────────► INSERT into credit.bureau_enquiries
                               (immutable; ADR-048 Cat 1 trigger)

Module classification

Application Lambda — synchronous request/response, exposed via Function URL with auth_type=AWS_IAM. v1 ships without API Gateway; when MOD-075 (internal API gateway) lands, the URL endpoint is replaced with an APIGW route via SSM resolution and the published /bank/{env}/credit/bureau-enquiry/api-endpoint is unchanged.

Jurisdiction model

Per ADR-042, jurisdiction is data — supplied in the request body and persisted on every row. One Lambda runs both NZ and AU. No per-jurisdiction stack split.


3. Data plane

Tables owned

Table Mutability Owner of writes Notes
credit.bureau_enquiries Append-only (ADR-048 Cat 1) MOD-128 Inbound bureau enquiry log. Distinct from MOD-059's credit.credit_bureau_requests (outbound CCR submissions). Trigger trg_credit_bureau_enquiries_immutable blocks UPDATE/DELETE.
credit.bureau_consents Mutable (revocations) MOD-128 (stand-in) Stand-in for MOD-049 (Not started). Replaced by AP-010-pattern-1 read from MOD-049's published consent contract when it ships.
credit.bureau_config Mutable (operator) MOD-128 Per-(jurisdiction, product_type, enquiry_purpose) primary + fallback routing. Seed loaded for AU credit_assessment (Equifax AU primary, illion fallback) and NZ credit_assessment (Centrix primary).
credit.idempotency_keys Mutable (auto-expire) MOD-128 (created here; reused by MOD-027/028/029/031 etc.) Shared SD05 idempotency store. PK (key, module_id). 24h TTL.

Tables read (cross-domain via AP-010 pattern 1)

Owning domain View Purpose
SD02 (bank_kyc) banking.customer_relationships_identity_readable (party_id, kyc_status, cdd_tier, last_verified_at) FR-577 identity gate. Read via bank_kyc_readonly direct host. View is being added by docs/handoffs/MOD-104-bank-kyc-identity-view.handoff.md (request to bank-kyc). Until view ships, env var KYC_IDENTITY_VIEW_NAME is empty and the gate fails closed.

Schema (V001)

V001 creates the credit schema (first SD05 module to land in bank_credit Neon database) plus: - credit.fn_immutable_row() — shared ADR-048 Cat 1 helper used by MOD-128's enquiry trigger and reused by MOD-027/028/031/059 when those modules land. - credit.bureau_enquiries — primary write target. PK enquiry_id. Indexes on customer_id, (application_id) WHERE NOT NULL, request_at DESC, and a partial index for FR-578 suppression lookup. - credit.bureau_consents — stand-in. - credit.bureau_config — UNIQUE (jurisdiction, product_type, enquiry_purpose).

V002 creates credit.idempotency_keys.

Grants (MOD-103 role model)

bank_credit_app_user:    SELECT, INSERT  on credit.bureau_enquiries
                         SELECT, INSERT, UPDATE on credit.bureau_consents
                         SELECT          on credit.bureau_config
                         SELECT, INSERT, DELETE on credit.idempotency_keys
bank_credit_readonly:    SELECT          on all four tables
bank_credit_migrate_user: ALL (granted by MOD-103 default + V001/V002 DDL)

bank_credit_app_user deliberately does NOT have UPDATE or DELETE on credit.bureau_enquiries — defence-in-depth alongside the immutability trigger.


4. SSM outputs

SSM path Value Consumed by
/bank/{env}/credit/bureau-enquiry/function-arn Lambda ARN MOD-029 (when built), ops
/bank/{env}/credit/bureau-enquiry/function-name Lambda function name MOD-076 dashboard ingest, ops
/bank/{env}/credit/bureau-enquiry/api-endpoint Function URL (v1) / API Gateway URL (post-MOD-075) MOD-029 (when built)
/bank/{env}/credit/tables/bureau-enquiries/name credit.bureau_enquiries MOD-029, MOD-042 (CDC)
/bank/{env}/credit/tables/idempotency-keys/name credit.idempotency_keys All SD05 SD05 Lambdas writing to GL

5. SSM inputs (upstream contract)

Eagerly resolved at IaC plan time. Missing values throw with the path AND the owning upstream module BEFORE any resource is queued.

Path Owner
/bank/{env}/iam/lambda/bank-credit/arn MOD-104
/bank/{env}/eventbridge/bank-credit/arn MOD-104
/bank/{env}/kms/pii/arn MOD-104
/bank/{env}/observability/adot-nodejs-arm64-arn MOD-076
Secrets Manager bank-neon/{env}/bank_credit/app_user MOD-103
Secrets Manager bank-neon/{env}/bank_kyc/readonly MOD-103

Optional (v1 unblocked when absent):

Path Owner
/bank/{env}/kyc/views/identity-readable/name bank-kyc (handoff MOD-104-bank-kyc-identity-view.handoff.md)
/bank/{env}/secrets/rotation/fn-arn MOD-045

Secrets owned by MOD-128

Secret KMS Rotation
bank-credit/{env}/equifax-au-credentials PII CMK 90d via MOD-045 rotation Lambda (when available)
bank-credit/{env}/illion-credentials PII CMK 90d
bank-credit/{env}/centrix-credentials PII CMK 90d
bank-credit/{env}/equifax-nz-credentials PII CMK 90d
bank-credit/{env}/experian-au-credentials PII CMK 90d

Each tagged module=MOD-128, category=bureau-credential, rotation_enabled=true, secret_type=API_KEY. dev/uat ship with placeholder JSON; prod values rotated in by orchestrator.


6. Events

  • Published: none in v1 (ruling #4).
  • Consumed: none.
  • Schema registry: no schema additions.

If MOD-076 wants bank.credit.bureau_enquiry_logged for dashboard fan-out, it lands at MOD-029 build time when the event payload shape can include the full caller context.


7. Acceptance criteria

FR / Policy Mode Test file Verification
FR-577 (consent gate) tests/integration/fr/fr-577-consent-gate.test.ts Negative cases (CONSENT_MISSING, CONSENT_EXPIRED) + identity gate negative (IDENTITY_NOT_VERIFIED). Positive complement gated on view-configured flag.
FR-578 (duplicate suppression) tests/integration/fr/fr-578-duplicate-suppression.test.ts Second enquiry within window returns suppressed=true and the cached score; bureau client not called twice.
FR-579 (multi-bureau fallback) tests/integration/fr/fr-579-fallback.test.ts AU primary returns no_file → handler routes to illion; persisted row records primary_bureau_failed=true and fallback_bureau_used=illion.
FR-580 (adverse-action shape) tests/integration/fr/fr-580-adverse-disclosure.test.ts Response shape includes adverse_flags, bureau_name, requires_disclosure=true on adverse outcome. (MOD-050 invocation is MOD-029's job — not exercised here.)
CRE-003 GATE tests/policy/pol-cre-003-gate.test.ts Contract-shape proof for the FK MOD-029 will gate on. End-to-end gate test lands at MOD-029 build.
PRI-001 GATE tests/policy/pol-pri-001-gate.test.ts Negative: missing/expired/revoked consent → 403, no bureau call. Source scan: no consent-bypass tokens.
CON-004 LOG tests/policy/pol-con-004-log.test.ts Captured + returned. Source scan: no MOD-050 invocation in MOD-128.
REP-010 LOG tests/policy/pol-rep-010-log.test.ts (unit) + tests/integration/infra/schema-immutability.test.ts (DB-level) Every code path persists a row; UPDATE/DELETE on bureau_enquiries fails.

Plus the standard unit suite (≥80% line coverage gate per ADR-045) and the contract test for the place-enquiry API.


8. v1 limitations & deferred work

  1. KYC identity view dependency. Until banking.customer_relationships_identity_readable is published by bank-kyc and the SSM path /bank/{env}/kyc/views/identity-readable/name is populated, MOD-128's identity gate fails closed (every enquiry returns IDENTITY_NOT_VERIFIED). Tracked in docs/handoffs/MOD-104-bank-kyc-identity-view.handoff.md. No MOD-128 redeploy required when the view ships — the env var is populated, the gate becomes live.

  2. Consent storage stand-in. credit.bureau_consents is owned by MOD-128 as a stand-in until MOD-049 (SD08 consent capture, Not started) ships and publishes a consent contract. Replacement is a one-file change to services/consent.ts. Tracked in docs/handoffs/MOD-128-complete.handoff.md.

  3. Real bureau API integration. All five bureau clients ship with deterministic stubs gated on placeholder credentials (same pattern as MOD-009). Real provider integration is a follow-up build (one client per provider) once production credentials are rotated in.

  4. No EventBridge events. v1 doesn't publish to the bank-credit bus. If observability dashboards need bureau-enquiry events, they're added at MOD-029 build time.

  5. Wiki SD05 data model gap. credit.bureau_enquiries is not in the published SD05 data model. Wiki update handoff filed in docs/handoffs/MOD-128-data-model-gap.handoff.md.

  6. Function URL surface. When MOD-075 internal API gateway ships, MOD-128 swaps Function URL for an APIGW target. Published /bank/{env}/credit/bureau-enquiry/api-endpoint stays the same.


9. Operational notes

  • Reserved concurrency: 10 dev / 30 prod (orchestrator ruling). Tunable per IaC.
  • Architecture: arm64 with ADOT arm64 layer (CLAUDE.md mandate).
  • Idempotency window: 24h. Same idempotency_key returns the stored response.
  • Bureau suppression window: 30 days (default) — configurable via BUREAU_SUPPRESSION_WINDOW_DAYS env var.
  • Logging retention: 1 year (prod) / 1 month (dev) per CloudWatch retention. Long-term archive via MOD-076 subscription filter.
  • Error classification (per error-handling-standard): consent and identity blocks → COMPLIANCE_BLOCK (HTTP 403, non-retryable). Validation → VALIDATION_FAILURE (422). Bureau timeouts → PROVIDER_ERROR (503, retryable). DB unavailable → TRANSIENT_INFRA (503, retryable).

10. Decision log

  • 2026-05-06 — Tier A build assignment (orchestrator). MOD-029 + MOD-050 removed from MOD-128.yaml deps after wiki correction. MOD-128 build proceeds independently of MOD-029.
  • 2026-05-06 — Identity-gate architecture decided as AP-010 pattern 1 (cross-domain published view) rather than HTTP API to MOD-009. Reason: MOD-009 exposes only a CREATE endpoint (POST /verify), not a status lookup; pattern 1 is AP-010-preferred and cheaper. Implementation includes a fail-closed adapter so MOD-128 stays unblocked while bank-kyc publishes the view.
  • 2026-05-06 — CON-004 mode confirmed LOG (not AUTO). MOD-128 captures adverse findings; MOD-029 invokes MOD-050. Wiki yaml updated by orchestrator.
  • 2026-05-06 — Bureau coverage: 5 clients implemented, 3 wired into default config (Equifax AU primary AU, illion fallback AU, Centrix primary NZ). Equifax NZ + Experian AU implemented with stub credentials; selectable per product type via credit.bureau_config.
  • 2026-05-06 — v1 surface = Function URL with AWS_IAM auth. Swaps to MOD-075 APIGW when that ships.
  • 2026-05-06 — Reserved concurrency 10 dev / 30 prod (per orchestrator).

  • Wiki spec: bank-wiki/source/entities/modules/MOD-128.{yaml,md}
  • Wiki design (MOD-128 long-form): bank-wiki/source/pages/design/modules/MOD-128.md
  • FR register: bank-wiki/source/pages/goals/fr-register.md — FR-577..580
  • Wiki update handoff (data model gap): docs/handoffs/MOD-128-data-model-gap.handoff.md
  • Cross-domain handoff (KYC identity view): docs/handoffs/MOD-104-bank-kyc-identity-view.handoff.md
  • Module completion handoff: docs/handoffs/MOD-128-complete.handoff.md