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-requireddefaultTags - 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_scheduleenforced 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 intests/integration/infra/review-schedule-immutability.test.ts
Out-of-scope / drift items¶
- 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.
- kyc_review_completed / overdue_block catalogue entries. Net-new. Wiki sync after build adds them to
event-catalogue.md. - 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.
last_kyc_check_idFK tokyc.kyc_checks(id). Left nullable text-uuid (no FK) to avoid cross-module migration ordering. Documented.- JSONB result_data shape. MOD-012 audit-trail consumer specifies the
outcome/rationale/updated_risk_factorskeys. Versioned at v1 by convention; future shape changes ride MOD-012's contract.