new-site/api/migrations/017_identity_verifications.sql
justin f8cd37ac8c Initial commit — Performance West telecom compliance platform
Includes: API (Express/TypeScript), Astro site, Python workers,
document generators, FCC compliance tools, Canada CRTC formation,
Ansible infrastructure, and deployment scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 06:54:22 -05:00

104 lines
4.5 KiB
PL/PgSQL

-- 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;