michangarrito/database/schemas/21-analytics.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

292 lines
8.7 KiB
PL/PgSQL

-- =============================================================================
-- MICHANGARRITO - 21 ANALYTICS (MCH-034, MCH-035: Analytics y Reportes)
-- =============================================================================
-- Sistema de metricas, analytics y generacion de reportes
-- Almacena metricas agregadas y permite exportacion de reportes
-- =============================================================================
-- Tipos de metrica
CREATE TYPE metric_type AS ENUM (
-- Ventas
'sales_total',
'sales_count',
'sales_average_ticket',
'sales_by_category',
'sales_by_payment_method',
-- Clientes
'customers_total',
'customers_new',
'customers_active',
'customers_retention_rate',
-- Fiados
'credits_total_amount',
'credits_pending_amount',
'credits_overdue_amount',
'credits_recovery_rate',
-- Inventario
'inventory_total_value',
'inventory_low_stock_count',
'inventory_turnover_rate',
-- Productos
'products_total',
'products_top_selling',
'products_slow_moving'
);
-- Tipos de periodo
CREATE TYPE metric_period AS ENUM (
'hourly',
'daily',
'weekly',
'monthly',
'yearly'
);
-- Metricas agregadas (pre-calculadas)
CREATE TABLE IF NOT EXISTS analytics.metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
-- Tipo y periodo
metric_type metric_type NOT NULL,
period_type metric_period NOT NULL,
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
-- Valor
value DECIMAL(15, 2) NOT NULL,
-- Metadata adicional (desglose, comparacion, etc.)
metadata JSONB DEFAULT '{}',
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW(),
-- Constraint: una metrica por tenant/tipo/periodo
UNIQUE(tenant_id, metric_type, period_type, period_start)
);
CREATE INDEX idx_analytics_metrics_tenant ON analytics.metrics(tenant_id);
CREATE INDEX idx_analytics_metrics_type ON analytics.metrics(metric_type);
CREATE INDEX idx_analytics_metrics_period ON analytics.metrics(period_start DESC);
CREATE INDEX idx_analytics_metrics_lookup ON analytics.metrics(tenant_id, metric_type, period_type, period_start DESC);
COMMENT ON TABLE analytics.metrics IS 'Metricas pre-calculadas para dashboards';
COMMENT ON COLUMN analytics.metrics.metadata IS 'Desglose adicional: {by_category: {...}, comparison: {...}}';
-- Eventos de analytics (tracking en tiempo real)
CREATE TABLE IF NOT EXISTS analytics.events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
-- Evento
event_type VARCHAR(100) NOT NULL,
event_name VARCHAR(255),
-- Propiedades
properties JSONB DEFAULT '{}',
-- Contexto
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
session_id VARCHAR(100),
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_analytics_events_tenant ON analytics.events(tenant_id);
CREATE INDEX idx_analytics_events_type ON analytics.events(event_type);
CREATE INDEX idx_analytics_events_created ON analytics.events(created_at DESC);
-- Particionado por fecha (para performance en grandes volumenes)
-- En produccion considerar particionado real
-- Usa funcion IMMUTABLE timestamptz_to_date() definida en 02-functions.sql
CREATE INDEX idx_analytics_events_date ON analytics.events(public.timestamptz_to_date(created_at));
COMMENT ON TABLE analytics.events IS 'Eventos de tracking para analytics detallado';
-- Tipos de reporte
CREATE TYPE report_type AS ENUM (
-- Ventas
'sales_summary',
'sales_detail',
'sales_by_product',
'sales_by_category',
'sales_by_period',
-- Inventario
'inventory_status',
'inventory_movements',
'inventory_valuation',
-- Clientes
'customers_list',
'customers_top',
'customers_inactive',
-- Fiados
'credits_pending',
'credits_overdue',
'credits_history',
-- Financiero
'cash_flow',
'profit_loss',
-- Operativo
'corte_caja'
);
-- Formato de exportacion
CREATE TYPE report_format AS ENUM (
'pdf',
'xlsx',
'csv',
'json'
);
-- Estado del reporte
CREATE TYPE report_status AS ENUM (
'pending',
'processing',
'completed',
'failed',
'expired'
);
-- Reportes generados
CREATE TABLE IF NOT EXISTS analytics.reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
-- Tipo y nombre
report_type report_type NOT NULL,
name VARCHAR(255) NOT NULL,
-- Parametros del reporte
parameters JSONB DEFAULT '{}',
-- Ejemplo: {date_from, date_to, category_id, include_details}
-- Resultado
status report_status DEFAULT 'pending',
file_format report_format,
file_url TEXT, -- URL firmada de storage
file_size INTEGER, -- bytes
-- Errores
error_message TEXT,
-- Solicitante
requested_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ -- Cuando expira el archivo
);
CREATE INDEX idx_analytics_reports_tenant ON analytics.reports(tenant_id);
CREATE INDEX idx_analytics_reports_status ON analytics.reports(status);
CREATE INDEX idx_analytics_reports_type ON analytics.reports(report_type);
CREATE INDEX idx_analytics_reports_created ON analytics.reports(created_at DESC);
COMMENT ON TABLE analytics.reports IS 'Reportes generados para exportacion';
COMMENT ON COLUMN analytics.reports.parameters IS 'Parametros usados para generar el reporte';
COMMENT ON COLUMN analytics.reports.expires_at IS 'Fecha de expiracion del archivo (auto-limpieza)';
-- Programacion de reportes (reportes automaticos)
CREATE TABLE IF NOT EXISTS analytics.report_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
-- Configuracion
name VARCHAR(255) NOT NULL,
report_type report_type NOT NULL,
file_format report_format DEFAULT 'pdf',
parameters JSONB DEFAULT '{}',
-- Programacion (cron-like)
schedule_type VARCHAR(20) NOT NULL, -- 'daily', 'weekly', 'monthly'
schedule_day INTEGER, -- 1-7 para weekly, 1-31 para monthly
schedule_hour INTEGER DEFAULT 8, -- Hora del dia (0-23)
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
-- Entrega
delivery_email VARCHAR(255), -- Email para enviar reporte
delivery_webhook_url TEXT, -- Webhook para notificar
-- Estado
is_active BOOLEAN DEFAULT true,
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
-- Timestamps
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_analytics_schedules_tenant ON analytics.report_schedules(tenant_id);
CREATE INDEX idx_analytics_schedules_next_run ON analytics.report_schedules(next_run_at) WHERE is_active = true;
CREATE TRIGGER update_analytics_report_schedules_updated_at
BEFORE UPDATE ON analytics.report_schedules
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
COMMENT ON TABLE analytics.report_schedules IS 'Programacion de reportes automaticos';
-- =============================================================================
-- FUNCIONES DE ANALYTICS
-- =============================================================================
-- Funcion para obtener metricas de un periodo
CREATE OR REPLACE FUNCTION analytics.get_metrics(
p_tenant_id UUID,
p_metric_type metric_type,
p_period_type metric_period,
p_start_date TIMESTAMPTZ,
p_end_date TIMESTAMPTZ
) RETURNS TABLE (
period_start TIMESTAMPTZ,
value DECIMAL(15, 2),
metadata JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT m.period_start, m.value, m.metadata
FROM analytics.metrics m
WHERE m.tenant_id = p_tenant_id
AND m.metric_type = p_metric_type
AND m.period_type = p_period_type
AND m.period_start >= p_start_date
AND m.period_start < p_end_date
ORDER BY m.period_start;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION analytics.get_metrics IS 'Obtiene metricas de un periodo especifico';
-- Funcion para limpiar reportes expirados
CREATE OR REPLACE FUNCTION analytics.cleanup_expired_reports()
RETURNS INTEGER AS $$
DECLARE
v_deleted INTEGER;
BEGIN
WITH deleted AS (
DELETE FROM analytics.reports
WHERE expires_at < NOW()
AND status = 'completed'
RETURNING id
)
SELECT COUNT(*) INTO v_deleted FROM deleted;
RETURN v_deleted;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION analytics.cleanup_expired_reports IS 'Elimina reportes expirados';