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¶
-
HEM stand-in.
credit.hem_benchmarksis 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 indocs/handoffs/bank-risk-platform-hem-ingestion-request.handoff.md. TheHemLookupservice interface is stable across the swap. -
Snowflake-side REP-005 traceability. The
credit.affordability_assessmentsrow is structurally CDC-eligible (append-only, in thecreditschema), but MOD-042 CDC requireswal_level=logicalon the bank_credit Neon project and areplicatorrole — neither yet provisioned by MOD-103. Tracked indocs/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. -
Income verification = caller-supplied. v1 trusts the
income_verification_methodfield; 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 streamscredit.transactionsinto a SD05 mirror. -
Identity gate is soft. When
KYC_IDENTITY_VIEW_NAMEenv 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. -
application_idnullable. FK tocredit.credit_applicationslands when MOD-029 ships V001. -
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_keyreturns 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_bpsso 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_bpsper orchestrator's self-contained-audit precision. - 2026-05-06 — V001 asserts (does not recreate) MOD-128's
credit.fn_immutable_row()+credit.idempotency_keysper orchestrator's "no bare CREATE" instruction.
Related artefacts¶
- 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