Skip to content

MOD-027 — Affordability calculator (technical design)

Module: MOD-027 — Affordability calculator System: SD05 — Credit Decisioning & Loan Platform Repo: bank-credit Type: Application Lambda + IaC + Flyway FR scope: FR-169, FR-170, FR-171, FR-172 Policies satisfied: CRE-002 (CALC), CRE-003 (LOG), CON-004 (LOG), REP-005 (LOG) ADRs in effect: ADR-001, ADR-007, ADR-022, ADR-023, ADR-031, ADR-038, ADR-042, ADR-043, ADR-044, ADR-045, ADR-048, ADR-051, ADR-052, ADR-053.


1. Purpose

MOD-027 is the affordability assessment engine for SD05. Synchronous Lambda invoked by MOD-029 (and future MOD-117 / MOD-128 callers). For each credit application it: validates inputs, looks up CDD tier from the SD02 published view, applies a verification-method haircut to declared income, substitutes HEM benchmark floors per RG 209.60, applies the jurisdiction-specific stress-test buffer (NZ CCCFA / AU NCCP), computes net disposable income + DTI, classifies the outcome PASS/MARGINAL/FAIL, and persists an immutable credit.affordability_assessments row.

The response includes proposed_repayment_monthly, proposed_repayment_total_interest, and proposed_repayment_total_cost so the caller (MOD-029) can hand them to MOD-050 for CON-004 disclosure. MOD-027 does not invoke MOD-050 (per orchestrator ruling — CON-004 is LOG mode here; MOD-029 is the GATE holder).


2. Architecture

caller (MOD-029 etc.)
       │  POST {Function URL}  (SigV4-signed, BankCreditRole-bearing)
   MOD-027 Lambda (arm64, ADOT, BankCreditRole)
       ├── Validate inputs (FR-169..172)
       ├── Idempotency check    → credit.idempotency_keys (MOD-128 V002)
       ├── Identity lookup      → SD02 banking.customer_relationships_identity_readable
       │                          (AP-010 pattern 1; soft fallback when view unconfigured)
       ├── Income haircut       → DECLARED 0.85, BANK_STATEMENT/OPEN_BANKING 0.95, others 1.0
       ├── HEM lookup           → credit.hem_benchmarks (stand-in for SD06 ingestion)
       ├── Expense substitution → max(declared, hem)
       ├── Stress test          → NZ: max(5%, contracted+buffer) | AU: contracted+300bps
       ├── Repayment calc       → amortising at stress rate (revolving = 3% min)
       ├── Disposable income    → income - expenses - debt; ndi_after_repayment
       ├── DTI evaluation       → product threshold; FR-171 hard reject (no override)
       └── Persist              → INSERT credit.affordability_assessments (immutable)

3. Data plane

Tables owned

Table Mutability Notes
credit.affordability_assessments Append-only (ADR-048 Cat 1) trg_affordability_assessments_immutable reuses credit.fn_immutable_row() from MOD-128 V001. Includes orchestrator-required stress_rate_applied + buffer_applied_bps for self-contained audit; proposed_repayment_total_interest + proposed_repayment_total_cost for caller's disclosure (CON-004 LOG).
credit.hem_benchmarks Mutable (operator-loaded) Stand-in for SD06 HEM ingestion. Replaced by AP-010 pattern 1 cross-domain view when bank-risk-platform's HEM ingestion module ships. Same swap pattern as MOD-128's bureau_consents.

Tables read

Owning domain Resource Purpose
SD05 credit.idempotency_keys (created by MOD-128 V002) Shared idempotency store; MOD-027 writes with module_id="MOD-027".
SD02 bank_kyc.banking.customer_relationships_identity_readable Cross-domain identity status (party_id, kyc_status, cdd_tier, last_verified_at). Read via bank_kyc_readonly direct host. Fail-closed adapter when env var unset.

V001 prerequisites

V001 asserts (does NOT recreate) MOD-128's resources: - credit.fn_immutable_row() (MOD-128 V001) - credit.idempotency_keys (MOD-128 V002)

Per orchestrator ruling: missing prerequisites → RAISE EXCEPTION with a clear message.

Data-model gap (handoff filed)

credit.affordability_assessments adds 5 columns beyond the wiki SD05 data model: - stress_rate_applied, buffer_applied_bps, regulatory_framework — orchestrator's self-contained-audit precision - proposed_repayment_total_interest, proposed_repayment_total_cost — CON-004 LOG inputs to MOD-029 → MOD-050 - dti_threshold, decline_reason_codes, policy_refs, trace_id, term_months, contracted_rate, requested_amount — completeness for FR-172 audit

Plus credit.hem_benchmarks (entirely new). Wiki update handoff in docs/handoffs/MOD-027-data-model-gap.handoff.md.

application_id is nullable in v1 because credit.credit_applications (owned by MOD-029) does not yet exist. FK constraint added by MOD-029's V001 once that table exists.


4. SSM I/O

Upstream (consumed)

Path Owner
/bank/{env}/iam/lambda/bank-credit/arn MOD-104
/bank/{env}/kms/pii/arn MOD-104
/bank/{env}/observability/adot-nodejs-arm64-arn MOD-076
/bank/{env}/neon/direct-host (Flyway) MOD-103
Secret bank-neon/{env}/bank_credit/app_user MOD-103
Secret bank-neon/{env}/bank_credit/bank_credit_migrate_user (Flyway) MOD-103
Secret bank-neon/{env}/bank_kyc/readonly (cross-domain identity view; same convention as MOD-128) MOD-103
/bank/{env}/kyc/views/identity-readable/name bank-kyc (optional; soft-fallback when absent)

Downstream (published)

Path Value
/bank/{env}/credit/affordability/function-arn Lambda ARN
/bank/{env}/credit/affordability/function-name Lambda name
/bank/{env}/credit/affordability/api-endpoint Function URL (v1; APIGW post-MOD-075)
/bank/{env}/credit/tables/affordability-assessments/name credit.affordability_assessments
/bank/{env}/credit/tables/hem-benchmarks/name credit.hem_benchmarks

5. Acceptance criteria

FR / Policy Mode Test file Verification
FR-169 (NDI calc) tests/integration/fr/fr-169-net-disposable.test.ts Asserts NDI = net_income − assessed_expenses − existing_debt; ndi_after_repayment = NDI − proposed_repayment.
FR-170 (stress buffers) tests/integration/fr/fr-170-stress-buffers.test.ts AU = NCCP+300bps; NZ = CCCFA with 5% floor when contracted < floor.
FR-171 (DTI rejection) tests/integration/fr/fr-171-dti-rejection.test.ts Synthetic over-leverage scenario → FAIL with DTI_THRESHOLD_BREACHED.
FR-172 (audit completeness) tests/integration/fr/fr-172-audit-record.test.ts Response includes every audit-required field.
CRE-002 CALC tests/policy/pol-cre-002-calc.test.ts Fixture: known inputs → expected outputs (PASS, HEM-substitution, NZ floor).
CRE-003 LOG tests/policy/pol-cre-003-log.test.ts Every PASS + FAIL persists a row with all FR-172 fields.
CON-004 LOG tests/policy/pol-con-004-log.test.ts Response + persisted row carry total_interest + total_cost. Source scan: no MOD-050 invocation in src/.
REP-005 LOG tests/policy/pol-rep-005-log.test.ts Row carries stress_rate_applied, buffer_applied_bps, hem_source_version, regulatory_framework.

Plus the standard unit suite (≥80% line coverage) and the contract test for the calculate-affordability API.


6. v1 limitations & deferred work

  1. HEM stand-in. credit.hem_benchmarks is owned by MOD-027 with placeholder values. Replaced by AP-010 pattern 1 cross-domain view when bank-risk-platform's HEM ingestion module ships. Tracked in docs/handoffs/bank-risk-platform-hem-ingestion-request.handoff.md. The HemLookup service interface is stable across the swap.

  2. Snowflake-side REP-005 traceability. The credit.affordability_assessments row is structurally CDC-eligible (append-only, in the credit schema), but MOD-042 CDC requires wal_level=logical on the bank_credit Neon project and a replicator role — neither yet provisioned by MOD-103. Tracked in docs/handoffs/MOD-103-bank-credit-replicator.handoff.md. Per orchestrator ruling: this does NOT block MOD-027 Built — REP-005's structural obligation (immutable record exists) is satisfied at deploy.

  3. Income verification = caller-supplied. v1 trusts the income_verification_method field; OPEN_BANKING and BANK_STATEMENT both apply a 5% haircut without parsing transaction history. Real CDR transaction analysis is a v2 enhancement when MOD-042 streams credit.transactions into a SD05 mirror.

  4. Identity gate is soft. When KYC_IDENTITY_VIEW_NAME env var is empty (bank-kyc handoff not yet landed), the identity lookup returns null and the calc proceeds. Affordability is CALC, not GATE — MOD-029 owns the credit-decision GATE that consumes the result.

  5. application_id nullable. FK to credit.credit_applications lands when MOD-029 ships V001.

  6. Reserved concurrency unset in dev. AWS account 100-unit unreserved floor blocks dev reservation (same as MOD-128). Prod sets 30 once account headroom increases.


7. Operational notes

  • Architecture: arm64 + ADOT arm64 layer.
  • Idempotency: 24h window; same idempotency_key returns stored result.
  • Error classification: VALIDATION_FAILURE (422), COMPLIANCE_BLOCK (403; reserved — affordability rejection is currently 200 with FAIL outcome per FR-171's "decline reason" wording), TRANSIENT_INFRA / PROVIDER_ERROR (503, retryable).
  • Logging retention: 1 year prod / 1 month dev (MOD-076 archival to S3 for the 7-year tail).
  • Stress-test config: NZ floor + buffer + AU buffer are AppConfig-overridable; defaults match CCCFA / APRA standards. Per orchestrator precision the row carries stress_rate_applied + buffer_applied_bps so audit doesn't depend on AppConfig history.

8. Decision log

  • 2026-05-06 — Tier A build assignment after MOD-128 Deployed.
  • 2026-05-06 — CON-004 = LOG (capture + return); MOD-029 invokes MOD-050. Yaml corrected by orchestrator. Same precedent as MOD-128.
  • 2026-05-06 — REP-005 LOG = structural; replicator handoff filed but not blocking Built.
  • 2026-05-06 — HEM = own stand-in; handoff to bank-risk-platform (not MOD-085 — different data class).
  • 2026-05-06 — Stress test row carries stress_rate_applied + buffer_applied_bps per orchestrator's self-contained-audit precision.
  • 2026-05-06 — V001 asserts (does not recreate) MOD-128's credit.fn_immutable_row() + credit.idempotency_keys per orchestrator's "no bare CREATE" instruction.

  • Wiki spec: bank-wiki/source/entities/modules/MOD-027.{yaml,md}
  • Wiki long-form design: bank-wiki/source/pages/design/modules/MOD-027.md (to be archived from this doc at handoff processing time)
  • FR register: FR-169..172
  • Module completion handoff: docs/handoffs/MOD-027-complete.handoff.md
  • Wiki data-model gap: docs/handoffs/MOD-027-data-model-gap.handoff.md
  • HEM ingestion request: docs/handoffs/bank-risk-platform-hem-ingestion-request.handoff.md
  • Replicator request: docs/handoffs/MOD-103-bank-credit-replicator.handoff.md