-- ============================================================================= -- MICHANGARRITO - 20 FEATURES (MCH-032: Feature Flags por Plan) -- ============================================================================= -- Sistema de feature flags para controlar funcionalidades por plan/tenant -- Permite habilitar/deshabilitar features sin deployments -- ============================================================================= -- Categorias de features CREATE TYPE feature_category AS ENUM ( 'core', -- Funcionalidades core (siempre disponibles) 'analytics', -- Reportes y analytics 'automation', -- Automatizaciones 'integrations', -- Integraciones externas 'ai', -- Funcionalidades de IA 'advanced', -- Funcionalidades avanzadas 'beta' -- Features en beta ); -- Feature flags globales (definicion) CREATE TABLE IF NOT EXISTS features.flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificacion key VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, description TEXT, category feature_category DEFAULT 'core', -- Valor por defecto default_value BOOLEAN DEFAULT false, -- Planes donde esta habilitado plans_enabled TEXT[] DEFAULT '{}', -- Ejemplo: {'tiendita', 'tiendita_plus'} = habilitado para estos planes -- Vacio = usa default_value -- Control is_public BOOLEAN DEFAULT true, -- Si se muestra en UI de configuracion requires_restart BOOLEAN DEFAULT false, -- Si requiere reinicio del app -- Metadata metadata JSONB DEFAULT '{}', -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_features_flags_key ON features.flags(key); CREATE INDEX idx_features_flags_category ON features.flags(category); CREATE TRIGGER update_features_flags_updated_at BEFORE UPDATE ON features.flags FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE features.flags IS 'Definicion de feature flags globales'; COMMENT ON COLUMN features.flags.key IS 'Identificador unico del feature (snake_case)'; COMMENT ON COLUMN features.flags.plans_enabled IS 'Array de planes donde esta habilitado'; -- Feature flags por tenant (override) CREATE TABLE IF NOT EXISTS features.tenant_flags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, flag_id UUID NOT NULL REFERENCES features.flags(id) ON DELETE CASCADE, -- Override enabled BOOLEAN NOT NULL, -- Razon del override reason TEXT, -- Control expires_at TIMESTAMPTZ, -- Para features temporales (pruebas, demos) -- Audit created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, -- Timestamps created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), -- Constraints UNIQUE(tenant_id, flag_id) ); CREATE INDEX idx_features_tenant_flags_tenant ON features.tenant_flags(tenant_id); CREATE INDEX idx_features_tenant_flags_flag ON features.tenant_flags(flag_id); CREATE TRIGGER update_features_tenant_flags_updated_at BEFORE UPDATE ON features.tenant_flags FOR EACH ROW EXECUTE FUNCTION update_updated_at(); COMMENT ON TABLE features.tenant_flags IS 'Override de feature flags por tenant'; COMMENT ON COLUMN features.tenant_flags.reason IS 'Razon del override (promo, beta tester, etc.)'; COMMENT ON COLUMN features.tenant_flags.expires_at IS 'Fecha de expiracion del override'; -- ============================================================================= -- FUNCION: Verificar si un feature esta habilitado para un tenant -- ============================================================================= CREATE OR REPLACE FUNCTION features.is_enabled( p_tenant_id UUID, p_flag_key VARCHAR(100) ) RETURNS BOOLEAN AS $$ DECLARE v_flag RECORD; v_override RECORD; v_tenant_plan VARCHAR(50); BEGIN -- Obtener la definicion del flag SELECT * INTO v_flag FROM features.flags WHERE key = p_flag_key; IF NOT FOUND THEN RETURN false; -- Flag no existe = deshabilitado END IF; -- Buscar override para el tenant SELECT * INTO v_override FROM features.tenant_flags tf WHERE tf.tenant_id = p_tenant_id AND tf.flag_id = v_flag.id AND (tf.expires_at IS NULL OR tf.expires_at > NOW()); IF FOUND THEN RETURN v_override.enabled; -- Usar override END IF; -- Obtener plan del tenant SELECT s.plan_id INTO v_tenant_plan FROM subscriptions.subscriptions s WHERE s.tenant_id = p_tenant_id AND s.status = 'active' ORDER BY s.created_at DESC LIMIT 1; -- Verificar si el plan esta en la lista de planes habilitados IF v_tenant_plan IS NOT NULL AND v_flag.plans_enabled IS NOT NULL AND array_length(v_flag.plans_enabled, 1) > 0 THEN RETURN v_tenant_plan = ANY(v_flag.plans_enabled); END IF; -- Usar valor por defecto RETURN v_flag.default_value; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION features.is_enabled IS 'Verifica si un feature esta habilitado para un tenant'; -- ============================================================================= -- SEEDS: Feature flags iniciales -- ============================================================================= INSERT INTO features.flags (key, name, description, category, default_value, plans_enabled) VALUES -- Core (siempre disponibles) ('pos_basic', 'Punto de Venta Basico', 'Registro de ventas y carrito', 'core', true, '{}'), ('catalog_basic', 'Catalogo Basico', 'Gestion de productos y categorias', 'core', true, '{}'), -- Changarrito (plan basico) ('inventory_basic', 'Control de Inventario', 'Stock y alertas de bajo inventario', 'core', true, ARRAY['changarrito', 'tiendita', 'tiendita_plus']), ('customers_basic', 'Gestion de Clientes', 'Registro y historial de clientes', 'core', true, ARRAY['changarrito', 'tiendita', 'tiendita_plus']), -- Tiendita (plan medio) ('fiados', 'Sistema de Fiados', 'Credito a clientes con recordatorios', 'advanced', false, ARRAY['tiendita', 'tiendita_plus']), ('whatsapp_orders', 'Pedidos por WhatsApp', 'Recibir pedidos via WhatsApp', 'integrations', false, ARRAY['tiendita', 'tiendita_plus']), ('ai_assistant_basic', 'Asistente IA Basico', 'Consultas y reportes via chat', 'ai', false, ARRAY['tiendita', 'tiendita_plus']), -- Tiendita Plus (plan premium) ('analytics_advanced', 'Analytics Avanzado', 'Dashboards y metricas detalladas', 'analytics', false, ARRAY['tiendita_plus']), ('reports_export', 'Exportacion de Reportes', 'Exportar reportes PDF/Excel/CSV', 'analytics', false, ARRAY['tiendita_plus']), ('webhooks', 'Webhooks', 'Notificaciones a sistemas externos', 'integrations', false, ARRAY['tiendita_plus']), ('multi_location', 'Multi-sucursal', 'Gestion de multiples ubicaciones', 'advanced', false, ARRAY['tiendita_plus']), ('ai_assistant_advanced', 'Asistente IA Avanzado', 'Predicciones y recomendaciones', 'ai', false, ARRAY['tiendita_plus']), -- Beta ('delivery_tracking', 'Tracking de Entregas', 'Seguimiento de entregas en tiempo real', 'beta', false, '{}'), ('marketplace', 'Marketplace Proveedores', 'Conexion con distribuidores', 'beta', false, '{}') ON CONFLICT (key) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description, plans_enabled = EXCLUDED.plans_enabled, updated_at = NOW();