-- 017_identity_verifications.sql -- Stripe Identity verification sessions for director KYC. -- -- Every CRTC order director is verified via Stripe Identity BEFORE the order is -- saved and before any payment is collected. The verification session extracts -- name and date of birth from the ID document, which are compared against what -- the customer entered on the order form. -- -- Applies to ALL payment methods (card, ACH, Klarna, crypto). -- -- Match result tiers: -- name_match: exact | fuzzy_pass | fuzzy_warn | mismatch -- dob_match: exact | no_dob_on_id | mismatch -- overall: verified | pending | needs_review | failed -- -- 'needs_review' orders are held in a manual queue — payment is NOT collected -- until an admin clears them. This is a hard gate. BEGIN; CREATE TABLE IF NOT EXISTS identity_verifications ( id SERIAL PRIMARY KEY, -- Stripe Identity stripe_session_id TEXT NOT NULL UNIQUE, stripe_report_id TEXT, -- verification_report id once complete stripe_status TEXT, -- 'requires_input' | 'processing' | 'verified' | 'canceled' -- What customer typed on the form form_director_name TEXT NOT NULL, form_director_dob DATE, -- null if customer didn't enter DOB -- What Stripe extracted from the ID document id_first_name TEXT, id_last_name TEXT, id_full_name_extracted TEXT, -- first + last concatenated id_dob_year INTEGER, id_dob_month INTEGER, id_dob_day INTEGER, id_doc_type TEXT, -- 'driving_license' | 'passport' | 'id_card' id_issuing_country TEXT, id_expiry_year INTEGER, id_expiry_month INTEGER, id_expiry_day INTEGER, id_number TEXT, -- redacted after comparison -- Comparison results name_match_score NUMERIC(5,2), -- 0-100 fuzzy score name_match TEXT -- 'exact' | 'fuzzy_pass' | 'fuzzy_warn' | 'mismatch' | 'pending' CHECK (name_match IN ('exact','fuzzy_pass','fuzzy_warn','mismatch','pending')), dob_match TEXT -- 'exact' | 'no_dob_on_id' | 'mismatch' | 'pending' CHECK (dob_match IN ('exact','no_dob_on_id','mismatch','pending')), doc_expired BOOLEAN DEFAULT FALSE, -- Overall gate result overall_result TEXT NOT NULL DEFAULT 'pending' CHECK (overall_result IN ('pending','verified','needs_review','failed')), -- Admin review reviewed_by TEXT, review_notes TEXT, reviewed_at TIMESTAMPTZ, admin_override BOOLEAN DEFAULT FALSE, -- admin manually cleared needs_review -- Linkage order_number TEXT, -- set once order is created order_type TEXT DEFAULT 'canada_crtc', customer_email TEXT, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), verified_at TIMESTAMPTZ, ip_address TEXT, user_agent TEXT ); CREATE INDEX IF NOT EXISTS idx_iv_session ON identity_verifications (stripe_session_id); CREATE INDEX IF NOT EXISTS idx_iv_order ON identity_verifications (order_number); CREATE INDEX IF NOT EXISTS idx_iv_result ON identity_verifications (overall_result, created_at); CREATE INDEX IF NOT EXISTS idx_iv_needs_review ON identity_verifications (overall_result) WHERE overall_result = 'needs_review' AND admin_override = FALSE; -- Add identity_session_id to CRTC orders so the order route can gate on it ALTER TABLE canada_crtc_orders ADD COLUMN IF NOT EXISTS identity_session_id TEXT REFERENCES identity_verifications(stripe_session_id) ON DELETE SET NULL; ALTER TABLE canada_crtc_orders ADD COLUMN IF NOT EXISTS identity_result TEXT CHECK (identity_result IN ('verified','needs_review','failed','pending')); -- Admin view: sessions awaiting review CREATE OR REPLACE VIEW identity_pending_review AS SELECT iv.*, o.order_number AS linked_order, o.customer_email AS order_email FROM identity_verifications iv LEFT JOIN canada_crtc_orders o ON o.identity_session_id = iv.stripe_session_id WHERE iv.overall_result = 'needs_review' AND iv.admin_override = FALSE ORDER BY iv.created_at DESC; COMMIT;