trading-platform-database-v2/ddl/schemas/users/tables/002_profiles.sql
rckrdmrd b86dfa2e06 [DDL] feat: Sprint 1 - Add 12 tables for users, admin, notifications, market_data
## 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>
2026-01-16 19:41:53 -06:00

129 lines
4.7 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: users
-- TABLE: profiles
-- DESCRIPTION: Informacion extendida del perfil de usuario
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Tabla de Perfiles extendidos
CREATE TABLE IF NOT EXISTS users.profiles (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users.users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Informacion personal
bio TEXT,
date_of_birth DATE,
gender VARCHAR(20),
nationality VARCHAR(100),
country_of_residence VARCHAR(100),
city VARCHAR(100),
address TEXT,
postal_code VARCHAR(20),
-- Informacion profesional
occupation VARCHAR(100),
company_name VARCHAR(200),
annual_income_range VARCHAR(50), -- '0-25k', '25k-50k', '50k-100k', '100k-250k', '250k+'
source_of_funds VARCHAR(100), -- 'salary', 'business', 'investments', 'inheritance', 'other'
-- Documentacion
id_document_type VARCHAR(50), -- 'passport', 'national_id', 'drivers_license'
id_document_number VARCHAR(100),
id_document_expiry DATE,
id_document_country VARCHAR(100),
-- Redes sociales
social_links JSONB DEFAULT '{}'::JSONB, -- { "twitter": "@user", "linkedin": "..." }
-- Preferencias de comunicacion
preferred_contact_method VARCHAR(20) DEFAULT 'email', -- 'email', 'phone', 'sms'
timezone VARCHAR(50) DEFAULT 'America/New_York',
locale VARCHAR(10) DEFAULT 'es-MX',
-- Completitud del perfil
completion_percentage INTEGER NOT NULL DEFAULT 0 CHECK (completion_percentage BETWEEN 0 AND 100),
last_profile_update TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE users.profiles IS
'Informacion extendida del perfil de usuario, separada de credenciales';
COMMENT ON COLUMN users.profiles.completion_percentage IS
'Porcentaje de completitud del perfil (0-100)';
COMMENT ON COLUMN users.profiles.annual_income_range IS
'Rango de ingresos anuales para compliance KYC';
-- Indices
CREATE INDEX IF NOT EXISTS idx_profiles_user_id
ON users.profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_profiles_tenant_id
ON users.profiles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_profiles_completion
ON users.profiles(completion_percentage);
CREATE INDEX IF NOT EXISTS idx_profiles_country
ON users.profiles(country_of_residence);
-- Trigger para updated_at
DROP TRIGGER IF EXISTS profile_updated_at ON users.profiles;
CREATE TRIGGER profile_updated_at
BEFORE UPDATE ON users.profiles
FOR EACH ROW
EXECUTE FUNCTION users.update_user_timestamp();
-- Trigger para calcular completion_percentage
CREATE OR REPLACE FUNCTION users.calculate_profile_completion()
RETURNS TRIGGER AS $$
DECLARE
v_total_fields INTEGER := 10;
v_filled_fields INTEGER := 0;
BEGIN
IF NEW.bio IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.date_of_birth IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.nationality IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.country_of_residence IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.city IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.occupation IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.annual_income_range IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.source_of_funds IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.id_document_type IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
IF NEW.timezone IS NOT NULL THEN v_filled_fields := v_filled_fields + 1; END IF;
NEW.completion_percentage := (v_filled_fields * 100) / v_total_fields;
NEW.last_profile_update := NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS profile_completion_calc ON users.profiles;
CREATE TRIGGER profile_completion_calc
BEFORE INSERT OR UPDATE ON users.profiles
FOR EACH ROW
EXECUTE FUNCTION users.calculate_profile_completion();
-- RLS Policy para multi-tenancy
ALTER TABLE users.profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY profiles_tenant_isolation ON users.profiles
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON users.profiles TO trading_app;
GRANT SELECT ON users.profiles TO trading_readonly;