Skip to content

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-required defaultTags
  • 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:

  1. identity — MOD-009 must have produced a VERIFIED kyc_status + a PASS INITIAL_EIDV check
  2. sanctions — MOD-013's kyc.sanctions_results latest result_status must be CLEAR or FALSE_POSITIVE
  3. pep_edd — PEP customer (regulatory.party_regulatory_profiles) must have edd_completed_at set (AML-004)
  4. fraud_score — onboarding_fraud_score below per-product threshold (MOD-160 dependency; v1 short-circuits on null)
  5. cdd_tier — banking.customer_relationships.cdd_tier ≥ MOD-127's product min_cdd_tier (AML-002)
  6. risk_score — party.risk_scores_mirror composite_risk_score below per-product threshold + risk_tier ≠ CRITICAL
  7. jurisdiction — banking.customer_relationships.jurisdiction not in product's excluded_jurisdictions (license restrictions)
  8. 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_decided v1 — 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

  1. MOD-007 v2 follow-on (OQ-4). FR-712 enforcement is at the calling layer in v1 — the check-activation endpoint exists, but bank-core's transition-engine-pure.ts PENDING→ACTIVE doesn't yet call it. Build-time handoff: docs/handoffs/MOD-153-mod007-activation-gate.handoff.md.
  2. 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.
  3. MOD-063 adverse-action consumer rule (OQ-5). v1 stub via SNS; handoff to bank-platform for the consumer rule.
  4. MOD-104 PutEvents grant on bank-platform bus (k-3). Required for the system_decision_recorded cross-bus publish. SST deploy will fail AccessDenied until the grant lands; handoff filed.
  5. 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.
  6. Onboarding fraud score (rule 4). Wired but currently always passes — onboarding_fraud_score is null until MOD-160 ships a signal. New module dependency to track.
  7. Date-of-birth on the input snapshot. product_suitability v1 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 in tests/integration/infra/acceptance-decisions-immutability.test.ts