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