Skip to content

MOD-011 — KYC Periodic Review Scheduler

System: SD02 Customer Identity & KYC Platform Repo: bank-kyc Phase: 4 Status: Built (2026-04-30) Module type: hybrid (IaC + application Lambda)


Purpose

Owns the bank's recurring KYC review guarantee. Maintains one row per party in kyc.periodic_review_schedule with the next_review_due date computed from the customer's CDD tier (FR-085). A daily sweep emits 60/30/7-day reminders and on-due escalations (FR-086), publishes an overdue-block event after a 14-day grace period (FR-087), and records review completions to the KYC audit trail (FR-088).

FR coverage

FR Mechanism
FR-085 cadenceForTier — Simplified 1095d / Standard 730d / Enhanced 365d. Schedules seeded on bank.kyc.identity_verified; re-cadenced on bank.kyc.cdd_tier_assigned.
FR-086 Daily sweep emits bank.kyc.kyc_review_due at exact-day milestones T-60 / T-30 / T-7 / T_DUE; flips schedule status to OVERDUE post-due (continues emitting).
FR-087 Sweep emits bank.kyc.kyc_review_overdue_block at +15d (>14d grace). MOD-007 consumes to restrict new-product origination.
FR-088 POST /kyc/reviews/complete records to kyc.kyc_checks (check_type='PERIODIC_REVIEW') carrying outcome + reviewer_id + rationale + updated_risk_factors; emits bank.kyc.kyc_review_completed for MOD-012.
NFR-024 kyc.periodic_review_schedule trigger refuses DELETE + UPDATE on immutable columns.

Architectural fit

Reuses every pattern from MOD-009/010/013/014:

  • SST v3 Ion + Pulumi (^3.3.0)
  • Per-module sst.config.ts (name: "bank-kyc-mod-011"); SCP-required defaultTags
  • Lambda runs as MOD-104 BankKycRole
  • Defensive upstream lookups via infra/lib/upstream.ts
  • MOD-043 schema-registry redirect — registers 3 v1 schemas
  • arm64 Parameters & Secrets Lambda Extension layer
  • OpenTelemetry/ADOT layer for traces; structured ADR-031 logger with PII redaction (extends to rationale)
  • Append-only kyc.periodic_review_schedule enforced by trigger

Triggers

Trigger Source Effect
scheduled.review-sweep EventBridge default bus (rate(1 day) prod / rate(7 days) non-prod) Sweeps schedules + expiring documents; emits milestone + overdue-block events
bank.kyc.identity_verified bank-kyc bus (MOD-009) Seeds kyc.periodic_review_schedule row at tier-default cadence
bank.kyc.cdd_tier_assigned bank-kyc bus (MOD-010) Re-cadences existing schedule, preserving last_review_at tenure
POST /kyc/reviews/complete API Gateway (IAM-auth) Records completion + advances next due

Data model

kyc.periodic_review_schedule (MOD-011-owned)

column type role
id uuid PK row identifier
party_id uuid UNIQUE one row per party
cdd_tier varchar(16) SIMPLIFIED / STANDARD / ENHANCED
last_review_at timestamptz NULL wall-clock of last completion (NULL until first review)
next_review_due date computed forward from last_review_at (or today, when null) by + review_cadence_days
review_cadence_days integer the FR-085 cadence in days
status varchar(16) SCHEDULED / OVERDUE / IN_PROGRESS / COMPLETED / SUPPRESSED
last_kyc_check_id uuid NULL reference to the most recent kyc.kyc_checks completion
seeded_by varchar(64) always 'MOD-011'
seeded_from_event_id varchar(128) upstream event id that seeded this row
trace_id varchar(128) otel trace id at seed time
created_at timestamptz row insert wall-clock
updated_at timestamptz row update wall-clock

Trigger kyc.fn_periodic_review_schedule_append_only permits only the cadence-relevant column updates (cdd_tier, next_review_due, review_cadence_days, status, last_kyc_check_id, last_review_at, updated_at). DELETE always refused.

kyc.kyc_checks (MOD-009-owned, written here)

INSERT one row per completion with check_type='PERIODIC_REVIEW', performed_by=reviewer_id, result_data jsonb carrying {outcome, rationale, updated_risk_factors}, and expires_at = next_review_due.

Events

Event Direction Notes
bank.kyc.identity_verified v1 consumed (MOD-009) Seeds schedule
bank.kyc.cdd_tier_assigned v1 consumed (MOD-010) Re-cadences schedule
bank.kyc.kyc_review_due v1 published At T-60/-30/-7/T_DUE/OVERDUE; also for AML-003 DOCUMENT_EXPIRY hits
bank.kyc.kyc_review_completed v1 published On POST /kyc/reviews/complete success
bank.kyc.kyc_review_overdue_block v1 published At +15d overdue (FR-087)

kyc_review_completed and kyc_review_overdue_block are net-new catalogue entries — MOD-014's wiki sync did the same for list_updated.

Idempotency

Trigger Key shape
sweep — milestone ${trigger_event_id}:${party_id}:${milestone}:${due_date}
sweep — overdue block block:${party_id}:${due_date}
identity_verified seed seed:${event_id}:${party_id}
cdd_tier_assigned recadence cadence:${event_id}:${party_id}
completion API caller-supplied idempotency_key

24h TTL on every entry. Replays return the stored result without re-publishing or re-writing.

SSM outputs

Path Value
/bank/{stage}/kyc/reviews/function-arn Lambda ARN
/bank/{stage}/kyc/reviews/function-name Lambda name
/bank/{stage}/kyc/reviews/complete-api-endpoint Completion API URL
/bank/{stage}/kyc/events/kyc-review-due/schema-arn v1 schema ARN
/bank/{stage}/kyc/events/kyc-review-completed/schema-arn v1 schema ARN
/bank/{stage}/kyc/events/kyc-review-overdue-block/schema-arn v1 schema ARN
/bank/{stage}/kyc/tables/periodic-review-schedule/name kyc.periodic_review_schedule

Policy satisfaction

Policy Mode Mechanism
AML-002 AUTO nextReviewDue is a pure function of tier + last_review_at + today; deterministic. Source-level negative test asserts no manual_calendar/skip_review/bypass tokens.
AML-003 ALERT Sweep reads kyc.identity_documents.expiry_date and emits kyc_review_due with review_type='DOCUMENT_EXPIRY' at the same milestone bands. Already-expired documents raise an SNS alert with AML_003_DOCUMENT_EXPIRED envelope to the alarm-intake topic.
CON-001 AUTO cadenceForTier returns the same cadence for every customer of the same tier; no per-customer override path.

Quality gates met

  • Unit tests: 96 passing
  • Coverage: 86.94% lines / 92.1% funcs / 84.3% branches / 86.94% statements (thresholds: 80 / 80 / 75 / 80)
  • Typecheck: clean
  • Integration tests: 1 per FR + 1 per policy + 5 infra + NFR-019 idempotency
  • NFR-024 immutability: trigger refuses UPDATE on immutable columns + DELETE on kyc.periodic_review_schedule; asserted in tests/integration/infra/review-schedule-immutability.test.ts

Out-of-scope / drift items

  1. Cadence drift vs. data model comment. SD02 column comment said "365 for Enhanced, 1095 for Standard"; MOD-011 follows FR-085 (Standard 730d). Wiki sync should update the column comment.
  2. kyc_review_completed / overdue_block catalogue entries. Net-new. Wiki sync after build adds them to event-catalogue.md.
  3. MOD-007 consumer for overdue_block. MOD-007 is in another repo (bank-core); the rule + consumer can be wired forward-compatibly there. The schema is registered now.
  4. last_kyc_check_id FK to kyc.kyc_checks(id). Left nullable text-uuid (no FK) to avoid cross-module migration ordering. Documented.
  5. JSONB result_data shape. MOD-012 audit-trail consumer specifies the outcome / rationale / updated_risk_factors keys. Versioned at v1 by convention; future shape changes ride MOD-012's contract.