## New Tables Created (Sprint 1 - DDL Roadmap Q1-2026) ### users schema (4 tables): - profiles: Extended user profile information - user_settings: User preferences and configurations - kyc_verifications: KYC/AML verification records - risk_profiles: Trading risk assessment profiles ### admin schema (3 tables): - admin_roles: Platform administrative roles - platform_analytics: Aggregated platform metrics - api_keys: Programmatic API access keys ### notifications schema (1 table): - notifications: Multi-channel notification system ### market_data schema (4 tables): - tickers: Financial instruments catalog - ohlcv_5m: 5-minute OHLCV price data - technical_indicators: Pre-calculated TA indicators - ohlcv_5m_staging: Staging table for data ingestion ## Features: - Multi-tenancy with RLS policies - Comprehensive indexes for query optimization - Triggers for computed fields and timestamps - Helper functions for common operations - Views for dashboard and reporting - Full GRANTS configuration Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
231 lines
8.1 KiB
PL/PgSQL
231 lines
8.1 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: users
|
|
-- TABLE: kyc_verifications
|
|
-- DESCRIPTION: Verificacion de identidad KYC (Know Your Customer)
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para estado de verificacion KYC
|
|
DO $$ BEGIN
|
|
CREATE TYPE users.kyc_status AS ENUM (
|
|
'not_started', -- No ha iniciado proceso
|
|
'pending', -- Documentos enviados, pendiente revision
|
|
'under_review', -- En proceso de revision manual
|
|
'approved', -- Aprobado completamente
|
|
'rejected', -- Rechazado (puede reintentar)
|
|
'expired', -- Verificacion expirada, requiere re-verificacion
|
|
'suspended' -- Suspendido por actividad sospechosa
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para nivel de verificacion
|
|
DO $$ BEGIN
|
|
CREATE TYPE users.kyc_level AS ENUM (
|
|
'none', -- Sin verificacion
|
|
'basic', -- Email + telefono verificado
|
|
'standard', -- Documento ID verificado
|
|
'enhanced', -- ID + prueba de direccion
|
|
'full' -- Verificacion completa con video
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de Verificaciones KYC
|
|
CREATE TABLE IF NOT EXISTS users.kyc_verifications (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Estado actual
|
|
status users.kyc_status NOT NULL DEFAULT 'not_started',
|
|
level users.kyc_level NOT NULL DEFAULT 'none',
|
|
|
|
-- Verificacion de Email
|
|
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
email_verified_at TIMESTAMPTZ,
|
|
email_verification_method VARCHAR(50), -- 'code', 'link'
|
|
|
|
-- Verificacion de Telefono
|
|
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
phone_verified_at TIMESTAMPTZ,
|
|
phone_verification_method VARCHAR(50), -- 'sms', 'call'
|
|
|
|
-- Verificacion de Documento ID
|
|
id_document_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
id_document_verified_at TIMESTAMPTZ,
|
|
id_document_type VARCHAR(50), -- 'passport', 'national_id', 'drivers_license'
|
|
id_document_number_hash VARCHAR(255), -- Hash del numero para verificacion
|
|
id_document_country VARCHAR(100),
|
|
id_document_expiry DATE,
|
|
id_document_front_url TEXT, -- URL segura al documento (encriptado)
|
|
id_document_back_url TEXT,
|
|
id_selfie_url TEXT, -- Selfie con documento
|
|
|
|
-- Verificacion de Direccion
|
|
address_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
address_verified_at TIMESTAMPTZ,
|
|
address_document_type VARCHAR(50), -- 'utility_bill', 'bank_statement', 'tax_document'
|
|
address_document_url TEXT,
|
|
address_document_date DATE, -- Fecha del documento (max 3 meses)
|
|
|
|
-- Verificacion de Video (enhanced)
|
|
video_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
video_verified_at TIMESTAMPTZ,
|
|
video_url TEXT,
|
|
video_liveness_score DECIMAL(5, 4), -- 0.0000 - 1.0000
|
|
|
|
-- Proveedor de verificacion externo
|
|
external_provider VARCHAR(50), -- 'sumsub', 'onfido', 'jumio'
|
|
external_verification_id VARCHAR(255),
|
|
external_status VARCHAR(50),
|
|
external_risk_score DECIMAL(5, 4),
|
|
|
|
-- Informacion de revision
|
|
reviewer_id UUID,
|
|
reviewed_at TIMESTAMPTZ,
|
|
review_notes TEXT,
|
|
rejection_reason TEXT,
|
|
rejection_codes JSONB, -- Array de codigos de rechazo
|
|
|
|
-- AML/PEP checks
|
|
aml_checked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
aml_checked_at TIMESTAMPTZ,
|
|
aml_result VARCHAR(50), -- 'clear', 'match', 'potential_match'
|
|
aml_details JSONB,
|
|
|
|
pep_checked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
pep_checked_at TIMESTAMPTZ,
|
|
pep_result VARCHAR(50),
|
|
pep_details JSONB,
|
|
|
|
sanctions_checked BOOLEAN NOT NULL DEFAULT FALSE,
|
|
sanctions_checked_at TIMESTAMPTZ,
|
|
sanctions_result VARCHAR(50),
|
|
sanctions_details JSONB,
|
|
|
|
-- Expiracion
|
|
expires_at TIMESTAMPTZ,
|
|
reminder_sent_at TIMESTAMPTZ,
|
|
|
|
-- Intentos
|
|
verification_attempts INTEGER NOT NULL DEFAULT 0,
|
|
max_attempts INTEGER NOT NULL DEFAULT 3,
|
|
locked_until TIMESTAMPTZ,
|
|
|
|
-- Metadata
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
device_fingerprint VARCHAR(255),
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
started_at TIMESTAMPTZ,
|
|
completed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT kyc_unique_user UNIQUE (user_id)
|
|
);
|
|
|
|
COMMENT ON TABLE users.kyc_verifications IS
|
|
'Registro de verificacion KYC de usuarios para compliance regulatorio';
|
|
|
|
COMMENT ON COLUMN users.kyc_verifications.level IS
|
|
'Nivel de verificacion alcanzado: none, basic, standard, enhanced, full';
|
|
|
|
COMMENT ON COLUMN users.kyc_verifications.external_risk_score IS
|
|
'Score de riesgo del proveedor externo (0-1, menor es mejor)';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_user_id
|
|
ON users.kyc_verifications(user_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_tenant_id
|
|
ON users.kyc_verifications(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_status
|
|
ON users.kyc_verifications(status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_level
|
|
ON users.kyc_verifications(level);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_expires_at
|
|
ON users.kyc_verifications(expires_at)
|
|
WHERE expires_at IS NOT NULL AND status = 'approved';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_pending_review
|
|
ON users.kyc_verifications(created_at)
|
|
WHERE status IN ('pending', 'under_review');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_kyc_aml_matches
|
|
ON users.kyc_verifications(tenant_id, aml_result)
|
|
WHERE aml_result IN ('match', 'potential_match');
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS kyc_updated_at ON users.kyc_verifications;
|
|
CREATE TRIGGER kyc_updated_at
|
|
BEFORE UPDATE ON users.kyc_verifications
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION users.update_user_timestamp();
|
|
|
|
-- Funcion para actualizar nivel de KYC automaticamente
|
|
CREATE OR REPLACE FUNCTION users.update_kyc_level()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Calcular nivel basado en verificaciones completadas
|
|
IF NEW.video_verified AND NEW.address_verified AND NEW.id_document_verified AND NEW.phone_verified AND NEW.email_verified THEN
|
|
NEW.level := 'full';
|
|
ELSIF NEW.address_verified AND NEW.id_document_verified AND NEW.phone_verified AND NEW.email_verified THEN
|
|
NEW.level := 'enhanced';
|
|
ELSIF NEW.id_document_verified AND (NEW.phone_verified OR NEW.email_verified) THEN
|
|
NEW.level := 'standard';
|
|
ELSIF NEW.email_verified AND NEW.phone_verified THEN
|
|
NEW.level := 'basic';
|
|
ELSE
|
|
NEW.level := 'none';
|
|
END IF;
|
|
|
|
-- Actualizar status si todas las verificaciones del nivel estan completas
|
|
IF NEW.level = 'full' AND OLD.status IN ('pending', 'under_review') THEN
|
|
NEW.status := 'approved';
|
|
NEW.completed_at := NOW();
|
|
-- Set expiration (typically 1-2 years)
|
|
NEW.expires_at := NOW() + INTERVAL '2 years';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS kyc_level_update ON users.kyc_verifications;
|
|
CREATE TRIGGER kyc_level_update
|
|
BEFORE UPDATE ON users.kyc_verifications
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION users.update_kyc_level();
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE users.kyc_verifications ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY kyc_tenant_isolation ON users.kyc_verifications
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Policy especial para revisores (pueden ver todos los pendientes)
|
|
CREATE POLICY kyc_reviewer_access ON users.kyc_verifications
|
|
FOR SELECT
|
|
USING (
|
|
current_setting('app.is_kyc_reviewer', true)::boolean = true
|
|
AND status IN ('pending', 'under_review')
|
|
);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON users.kyc_verifications TO trading_app;
|
|
GRANT SELECT ON users.kyc_verifications TO trading_readonly;
|