michangarrito/database/schemas/20-features.sql
rckrdmrd 5a49ad0185 [INTEGRATION] feat: Integrate template-saas scopes and database objects
## Documentation
- Align MCH-029 to MCH-032 with template-saas modules (SAAS-008 to SAAS-015)
- Create MCH-034 (Analytics) and MCH-035 (Reports) from SAAS-016/017
- Update PLAN-DESARROLLO.md with Phase 7 and 8
- Update _MAP.md indexes (35 total epics)

## Database (5 new schemas, 14 tables)
- Add storage schema: buckets, files, signed_urls
- Add webhooks schema: endpoints, deliveries
- Add audit schema: logs, retention_policies
- Add features schema: flags, tenant_flags (14 seeds)
- Add analytics schema: metrics, events, reports, report_schedules
- Add auth.oauth_connections for MCH-030
- Add timestamptz_to_date() IMMUTABLE function
- Update EXPECTED_SCHEMAS in recreate-database.sh

## Analysis Reports
- ANALISIS-INTEGRACION-TEMPLATE-SAAS-2026-01-13.md
- VALIDACION-COHERENCIA-2026-01-13.md
- GAP-ANALYSIS-BD-2026-01-13.md
- REPORTE-EJECUCION-2026-01-13.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 07:10:55 -06:00

183 lines
7.4 KiB
PL/PgSQL

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