MOD-153 — Customer acceptance engine¶
System: SD02 Customer Identity & KYC Platform
Repo: bank-kyc
Phase: 8
Module type: hybrid (IaC + application Lambda)
Purpose¶
The acceptance engine is the formal AML-011 / FATF customer-acceptance gate
between KYC verification and product activation. For every product
application it evaluates 8 ordered rules and emits one of four outcomes
(ACCEPT / DECLINE / REFER / HOLD_FOR_EDD). Every decision and full input
snapshot is persisted to kyc.acceptance_decisions (the AML-012 formal
risk rating record), mirrored to MOD-048's system decision log, and
downstream activation is gated on an ACCEPT existing for the
(party, product) pair.
Architectural fit¶
Reuses the bank-kyc Built patterns:
- SST v3 Ion + Pulumi caret-pinned
^3.3.0 - Per-module
sst.config.ts(name: "bank-kyc-mod-153"); SCP-requireddefaultTags - Lambda runs as MOD-104 BankKycRole
- Defensive upstream lookups via
infra/lib/upstream.ts - MOD-043 schema-registry — registers
bank.kyc.acceptance_decided(v1) - Append-only Postgres via Flyway V001 — ADR-048 immutability trigger
on
kyc.acceptance_decisions - x86_64 Lambda + amd64 ADOT layer (the choice MOD-009/010/013 settled on)
- OpenTelemetry/ADOT layer for traces; structured ADR-031 logger with
PII redaction (extends to
input_snapshot)
Trigger surfaces¶
| Trigger | Source | Effect |
|---|---|---|
bank.kyc.identity_verified |
bank-kyc bus | Initial evaluation entry-point |
bank.kyc.cdd_tier_assigned |
bank-kyc bus | Re-eval (CDD tier change), FR-716 |
bank.kyc.sanctions_match_found |
bank-kyc bus | Re-eval, FR-716 |
bank.kyc.sanctions_match_cleared |
bank-kyc bus | Re-eval (lift HOLD on FP cleared), FR-716 |
customer_risk_score_updated (HIGH/CRITICAL only) |
bank-risk-platform bus | Re-eval, FR-716 |
bank.app.product_config_applied |
bank-app bus | Cache invalidation only — no evaluation |
POST /kyc/acceptance/evaluate |
API Gateway (IAM-auth) | Manual evaluation trigger |
GET /kyc/acceptance/check-activation |
API Gateway (IAM-auth) | FR-712 service-layer enforcement surface (used by MOD-007 v2) |
Rule pipeline¶
8 rules in FR-709 order:
- identity — MOD-009 must have produced a VERIFIED kyc_status + a PASS INITIAL_EIDV check
- sanctions — MOD-013's
kyc.sanctions_resultslatest result_status must be CLEAR or FALSE_POSITIVE - pep_edd — PEP customer (regulatory.party_regulatory_profiles) must have edd_completed_at set (AML-004)
- fraud_score — onboarding_fraud_score below per-product threshold (MOD-160 dependency; v1 short-circuits on null)
- cdd_tier — banking.customer_relationships.cdd_tier ≥ MOD-127's product min_cdd_tier (AML-002)
- risk_score — party.risk_scores_mirror composite_risk_score below per-product threshold + risk_tier ≠ CRITICAL
- jurisdiction — banking.customer_relationships.jurisdiction not in product's excluded_jurisdictions (license restrictions)
- product_suitability — CON-006: retail credit products only; v1 REFERs all configured-min-age cases (DOB not on snapshot)
Combination: any DECLINE > any HOLD_FOR_EDD > any REFER > else ACCEPT.
Data model¶
kyc.acceptance_decisions is owned by MOD-153 V001 — see SD02 data
model for the canonical column list. Append-only per AML-012 + ADR-048
with the canonical kyc.fn_immutable_row() trigger.
| Read | Source |
|---|---|
banking.customer_relationships |
MOD-009/010 — kyc_status, cdd_tier, edd_completed_at, jurisdiction, aml_risk_rating |
kyc.kyc_checks |
MOD-009 — latest INITIAL_EIDV PASS for last_verified_at |
kyc.sanctions_results |
MOD-013 — latest result_status |
regulatory.party_regulatory_profiles |
MOD-009 V001 — pep_flag |
party.risk_scores_mirror |
MOD-010 V002 — composite_risk_score, risk_tier |
| Write | Notes |
|---|---|
kyc.acceptance_decisions |
Append-only INSERT; ON CONFLICT (idempotency_key) DO NOTHING for replay safety |
SSM outputs¶
| Path | Value |
|---|---|
/bank/{stage}/kyc/acceptance/function-arn |
Lambda ARN |
/bank/{stage}/kyc/acceptance/function-name |
Lambda name |
/bank/{stage}/kyc/acceptance/api-endpoint |
API Gateway base URL |
/bank/{stage}/kyc/acceptance/check-activation-url |
Full URL of the FR-712 enforcement endpoint |
/bank/{stage}/kyc/events/acceptance-decided/schema-arn |
EventBridge schema (v1) ARN |
/bank/{stage}/kyc/tables/acceptance-decisions/name |
kyc.acceptance_decisions |
Events published¶
bank.kyc.acceptance_decidedv1 — emitted to bank-kyc bus after every successful decision write. Consumers: MOD-007 v2 (activation gate), MOD-063 (adverse-action notice, FR-714), MOD-151/MOD-053 (case fan-out, FR-713).bank-platform.system_decision_recorded— emitted to bank-platform bus per MOD-048 spec (FR-715).decision_type = "KYC_ACCEPTANCE_DECISION".
FR coverage¶
| FR | Mechanism |
|---|---|
| FR-709 | 8-rule pipeline runs in canonical order; applied_rules array preserves it |
| FR-710 | DB CHECK on decision IN ('ACCEPT','DECLINE','REFER','HOLD_FOR_EDD') + engine combination ranks |
| FR-711 | kyc.acceptance_decisions schema (V001) carries every required field |
| FR-712 | GET /kyc/acceptance/check-activation API + check-activation-url SSM contract; MOD-007 v2 wires the gate |
| FR-713 | bank.kyc.acceptance_decided event filterable on decision IN (REFER, HOLD_FOR_EDD) + v1 SNS stub to MOD-076 alarm-intake |
| FR-714 | Same event with product_category=CREDIT filter; v1 SNS stub carrying 24h SLA timestamp |
| FR-715 | bank-platform.system_decision_recorded published BEFORE side-effect SNS stubs |
| FR-716 | 4 EventBridge re-eval consumer rules (cdd_tier_assigned, sanctions_match_found, sanctions_match_cleared, customer_risk_score_updated) |
Policy satisfaction¶
| Policy | Mode | Mechanism |
|---|---|---|
| AML-011 | GATE | No path to activation without getLatestDecision().decision === 'ACCEPT'; negative test in tests/policy/aml-011-gate.test.ts |
| AML-002 | GATE | cdd_tier rule: customer's cdd_tier < product's min_cdd_tier → DECLINE/CDD_TIER_INSUFFICIENT |
| AML-012 | CALC | Every decision row carries methodology_version + input_snapshot + applied_rules + triggered_rules + reason_codes |
| AML-004 | GATE | pep_edd rule: PEP + edd_completed_at IS NULL → HOLD_FOR_EDD/PEP_EDD_INCOMPLETE |
| CON-006 | GATE | product_suitability rule: credit product with min_age criteria → REFER/SUITABILITY_NOT_EVALUABLE |
v1 known gaps / drift items¶
- MOD-007 v2 follow-on (OQ-4). FR-712 enforcement is at the calling
layer in v1 — the
check-activationendpoint exists, but bank-core'stransition-engine-pure.tsPENDING→ACTIVE doesn't yet call it. Build-time handoff:docs/handoffs/MOD-153-mod007-activation-gate.handoff.md. - MOD-151 / MOD-053 case scope (OQ-2). v1 fan-out is via the acceptance_decided event + SNS stub. MOD-151's current scope doesn't cover AML REFER cases — handoff requests scope clarification.
- MOD-063 adverse-action consumer rule (OQ-5). v1 stub via SNS; handoff to bank-platform for the consumer rule.
- MOD-104 PutEvents grant on bank-platform bus (k-3). Required for
the
system_decision_recordedcross-bus publish. SST deploy will fail AccessDenied until the grant lands; handoff filed. - MOD-127 acceptance.* parameter keys (k-5). v1 reads from MOD-127 API but falls back to a hardcoded compile-time config map when those keys aren't seeded yet. Handoff requests the seed-extension PR.
- Onboarding fraud score (rule 4). Wired but currently always
passes —
onboarding_fraud_scoreis null until MOD-160 ships a signal. New module dependency to track. - Date-of-birth on the input snapshot.
product_suitabilityv1 refers all configured-min-age credit cases because date_of_birth isn't on AcceptanceInputs. Add to db.ts loadInputs join when MOD-153 v2 ships.
Quality gates met¶
- Unit tests: 97 passing
- Coverage: ≥80% lines / ≥80% funcs / ≥75% branches / ≥80% statements
- Typecheck: clean
- Integration tests: 1 per FR (8) + 1 per policy (5) + ssm-outputs + eventbridge-rules + immutability + idempotency
- Contract tests: 2 — acceptance_decided + system_decision_recorded
- NFR-024 immutability: trigger refuses UPDATE/DELETE +
chk_acceptance_decisions_*CHECK constraints; asserted intests/integration/infra/acceptance-decisions-immutability.test.ts