-- ============================================================================ -- Trading Platform - Feature Flags Schema -- Created: Sprint 2 - TASK-2026-01-30-ANALISIS-INTEGRACION -- Based on: template-saas SAAS-009 Feature Flags (simplified without multi-tenancy) -- ============================================================================ -- Create schema if not exists CREATE SCHEMA IF NOT EXISTS feature_flags; -- ============================================================================ -- ENUMS -- ============================================================================ -- Flag status enum DO $$ BEGIN CREATE TYPE feature_flags.flag_status AS ENUM ('disabled', 'enabled', 'percentage'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- Rollout stage enum DO $$ BEGIN CREATE TYPE feature_flags.rollout_stage AS ENUM ('development', 'beta', 'production'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; -- ============================================================================ -- TABLE: flags (Global feature flag definitions) -- ============================================================================ CREATE TABLE IF NOT EXISTS feature_flags.flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identification code VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, description TEXT, category VARCHAR(100) DEFAULT 'general', -- Status and Rollout status feature_flags.flag_status NOT NULL DEFAULT 'disabled', rollout_stage feature_flags.rollout_stage NOT NULL DEFAULT 'development', rollout_percentage INTEGER DEFAULT 0, default_value BOOLEAN NOT NULL DEFAULT false, -- Targeting (simplified - by plan or role) targeting_rules JSONB DEFAULT '[]', -- Example: [{"type": "plan", "operator": "in", "values": ["pro", "premium"]}] -- Metadata metadata JSONB DEFAULT '{}', tags JSONB DEFAULT '[]', -- Lifecycle is_permanent BOOLEAN NOT NULL DEFAULT false, expires_at TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by VARCHAR(100), -- Constraints CONSTRAINT valid_flag_code CHECK (code ~ '^[a-z][a-z0-9_]*$'), CONSTRAINT valid_percentage CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100) ); -- ============================================================================ -- TABLE: user_flags (User-level flag overrides) -- ============================================================================ CREATE TABLE IF NOT EXISTS feature_flags.user_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE, -- Override is_enabled BOOLEAN NOT NULL, reason VARCHAR(500), expires_at TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID, -- Unique constraint per user/flag CONSTRAINT unique_user_flag UNIQUE (user_id, flag_id) ); -- ============================================================================ -- TABLE: evaluations (Evaluation history for analytics) -- ============================================================================ CREATE TABLE IF NOT EXISTS feature_flags.evaluations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE, user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, -- Evaluation result flag_code VARCHAR(100) NOT NULL, result BOOLEAN NOT NULL, evaluation_reason VARCHAR(100) NOT NULL, -- Reasons: 'default', 'user_override', 'targeting_rule', 'percentage', 'expired' -- Context context JSONB DEFAULT '{}', -- Timestamp created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================================================ -- INDEXES -- ============================================================================ -- Flags indexes CREATE INDEX IF NOT EXISTS idx_flags_code ON feature_flags.flags(code); CREATE INDEX IF NOT EXISTS idx_flags_status ON feature_flags.flags(status) WHERE status != 'disabled'; CREATE INDEX IF NOT EXISTS idx_flags_category ON feature_flags.flags(category); -- User flags indexes CREATE INDEX IF NOT EXISTS idx_user_flags_user ON feature_flags.user_flags(user_id); CREATE INDEX IF NOT EXISTS idx_user_flags_flag ON feature_flags.user_flags(flag_id); -- Evaluations indexes (partitioned by date for analytics) CREATE INDEX IF NOT EXISTS idx_evaluations_flag ON feature_flags.evaluations(flag_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_evaluations_user ON feature_flags.evaluations(user_id, created_at DESC) WHERE user_id IS NOT NULL; -- ============================================================================ -- FUNCTION: evaluate_flag -- Evaluates a feature flag for a given user -- ============================================================================ CREATE OR REPLACE FUNCTION feature_flags.evaluate_flag( p_flag_code VARCHAR(100), p_user_id UUID DEFAULT NULL ) RETURNS BOOLEAN AS $$ DECLARE v_flag RECORD; v_user_override RECORD; v_result BOOLEAN; v_reason VARCHAR(100); BEGIN -- Get flag definition SELECT * INTO v_flag FROM feature_flags.flags WHERE code = p_flag_code; IF NOT FOUND THEN RETURN false; END IF; -- Check if expired IF v_flag.expires_at IS NOT NULL AND v_flag.expires_at < NOW() THEN v_result := false; v_reason := 'expired'; -- Log evaluation INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason) VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason); RETURN v_result; END IF; -- Check user override (highest priority) IF p_user_id IS NOT NULL THEN SELECT * INTO v_user_override FROM feature_flags.user_flags WHERE flag_id = v_flag.id AND user_id = p_user_id AND (expires_at IS NULL OR expires_at > NOW()); IF FOUND THEN v_result := v_user_override.is_enabled; v_reason := 'user_override'; -- Log evaluation INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason) VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason); RETURN v_result; END IF; END IF; -- Check flag status CASE v_flag.status WHEN 'disabled' THEN v_result := false; v_reason := 'default'; WHEN 'enabled' THEN v_result := true; v_reason := 'default'; WHEN 'percentage' THEN -- Simple percentage-based rollout using user_id hash IF p_user_id IS NOT NULL THEN v_result := (abs(hashtext(p_user_id::text)) % 100) < v_flag.rollout_percentage; ELSE v_result := (random() * 100) < v_flag.rollout_percentage; END IF; v_reason := 'percentage'; ELSE v_result := v_flag.default_value; v_reason := 'default'; END CASE; -- Log evaluation INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason) VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason); RETURN v_result; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- TRIGGER: Update updated_at on flags -- ============================================================================ -- DEPRECATED: Use public.update_updated_at() instead -- Migration: migrations/2026-02-03_unify_common_functions.sql -- Issue: DUP-003 -- Task: ST-2.3 -- ============================================================================ CREATE OR REPLACE FUNCTION feature_flags.update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_flags_updated_at ON feature_flags.flags; CREATE TRIGGER trg_flags_updated_at BEFORE UPDATE ON feature_flags.flags FOR EACH ROW EXECUTE FUNCTION feature_flags.update_timestamp(); DROP TRIGGER IF EXISTS trg_user_flags_updated_at ON feature_flags.user_flags; CREATE TRIGGER trg_user_flags_updated_at BEFORE UPDATE ON feature_flags.user_flags FOR EACH ROW EXECUTE FUNCTION feature_flags.update_timestamp(); -- ============================================================================ -- COMMENTS -- ============================================================================ COMMENT ON TABLE feature_flags.flags IS 'Global feature flag definitions'; COMMENT ON TABLE feature_flags.user_flags IS 'User-level feature flag overrides'; COMMENT ON TABLE feature_flags.evaluations IS 'Feature flag evaluation history for analytics'; COMMENT ON FUNCTION feature_flags.evaluate_flag IS 'Evaluates a feature flag for a given user with priority: user_override > percentage > default'; -- ============================================================================ -- INITIAL FLAGS (common flags for trading platform) -- ============================================================================ INSERT INTO feature_flags.flags (code, name, description, category, status, rollout_stage, default_value) VALUES ('enable_dark_mode', 'Dark Mode', 'Enable dark mode UI theme', 'ui', 'enabled', 'production', true), ('enable_advanced_charts', 'Advanced Charts', 'Enable advanced TradingView-like charts', 'trading', 'enabled', 'production', true), ('enable_ml_signals', 'ML Signals', 'Show ML prediction signals on charts', 'ml', 'enabled', 'production', true), ('enable_llm_assistant', 'LLM Assistant', 'Enable AI trading assistant chat', 'assistant', 'percentage', 'beta', false), ('enable_portfolio_rebalancing', 'Portfolio Rebalancing', 'Enable automatic portfolio rebalancing', 'portfolio', 'disabled', 'development', false), ('enable_mt4_gateway', 'MT4 Gateway', 'Enable MetaTrader 4 integration', 'trading', 'disabled', 'development', false), ('enable_2fa_enforcement', '2FA Enforcement', 'Require 2FA for all users', 'security', 'disabled', 'development', false), ('enable_paper_trading', 'Paper Trading', 'Enable paper trading mode', 'trading', 'enabled', 'production', true) ON CONFLICT (code) DO NOTHING;