-- ============================================================= -- ARCHIVO: 12-webhooks.sql -- DESCRIPCION: Sistema de webhooks, endpoints, entregas -- VERSION: 1.0.0 -- PROYECTO: ERP-Core V2 -- FECHA: 2026-01-10 -- EPIC: SAAS-INTEGRATIONS (EPIC-SAAS-005) -- HISTORIAS: US-060, US-061 -- ============================================================= -- ===================== -- SCHEMA: webhooks -- ===================== CREATE SCHEMA IF NOT EXISTS webhooks; -- ===================== -- TABLA: webhooks.event_types -- Tipos de eventos disponibles para webhooks -- ===================== CREATE TABLE IF NOT EXISTS webhooks.event_types ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificación code VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(200) NOT NULL, description TEXT, category VARCHAR(50), -- sales, inventory, auth, billing, system -- Schema del payload payload_schema JSONB DEFAULT '{}', -- Estado is_active BOOLEAN DEFAULT TRUE, is_internal BOOLEAN DEFAULT FALSE, -- Eventos internos no expuestos -- Metadata metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: webhooks.endpoints -- Endpoints configurados por tenant -- ===================== CREATE TABLE IF NOT EXISTS webhooks.endpoints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Identificación name VARCHAR(200) NOT NULL, description TEXT, -- URL destino url TEXT NOT NULL, http_method VARCHAR(10) DEFAULT 'POST', -- Autenticación auth_type VARCHAR(30) DEFAULT 'none', -- none, basic, bearer, hmac, oauth2 auth_config JSONB DEFAULT '{}', -- basic: {username, password} -- bearer: {token} -- hmac: {secret, header_name, algorithm} -- oauth2: {client_id, client_secret, token_url} -- Headers personalizados custom_headers JSONB DEFAULT '{}', -- Eventos suscritos subscribed_events TEXT[] NOT NULL DEFAULT '{}', -- Filtros filters JSONB DEFAULT '{}', -- Ejemplo: {"branch_id": ["uuid1", "uuid2"], "amount_gte": 1000} -- Configuración de reintentos retry_enabled BOOLEAN DEFAULT TRUE, max_retries INTEGER DEFAULT 5, retry_delay_seconds INTEGER DEFAULT 60, retry_backoff_multiplier DECIMAL(3,1) DEFAULT 2.0, -- Timeouts timeout_seconds INTEGER DEFAULT 30, -- Estado is_active BOOLEAN DEFAULT TRUE, is_verified BOOLEAN DEFAULT FALSE, verified_at TIMESTAMPTZ, -- Secreto para firma signing_secret VARCHAR(255), -- Estadísticas total_deliveries INTEGER DEFAULT 0, successful_deliveries INTEGER DEFAULT 0, failed_deliveries INTEGER DEFAULT 0, last_delivery_at TIMESTAMPTZ, last_success_at TIMESTAMPTZ, last_failure_at TIMESTAMPTZ, -- Rate limiting rate_limit_per_minute INTEGER DEFAULT 60, rate_limit_per_hour INTEGER DEFAULT 1000, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES auth.users(id), UNIQUE(tenant_id, url) ); -- ===================== -- TABLA: webhooks.deliveries -- Log de entregas de webhooks -- ===================== CREATE TABLE IF NOT EXISTS webhooks.deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint_id UUID NOT NULL REFERENCES webhooks.endpoints(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Evento event_type VARCHAR(100) NOT NULL, event_id UUID NOT NULL, -- Payload enviado payload JSONB NOT NULL, payload_hash VARCHAR(64), -- SHA-256 para deduplicación -- Request request_url TEXT NOT NULL, request_method VARCHAR(10) NOT NULL, request_headers JSONB DEFAULT '{}', -- Response response_status INTEGER, response_headers JSONB DEFAULT '{}', response_body TEXT, response_time_ms INTEGER, -- Estado status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, sending, delivered, failed, retrying, cancelled -- Reintentos attempt_number INTEGER DEFAULT 1, max_attempts INTEGER DEFAULT 5, next_retry_at TIMESTAMPTZ, -- Error info error_message TEXT, error_code VARCHAR(50), -- Timestamps scheduled_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: webhooks.events -- Cola de eventos pendientes de envío -- ===================== CREATE TABLE IF NOT EXISTS webhooks.events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Tipo de evento event_type VARCHAR(100) NOT NULL, -- Payload del evento payload JSONB NOT NULL, -- Contexto resource_type VARCHAR(100), resource_id UUID, triggered_by UUID REFERENCES auth.users(id), -- Estado status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, dispatched, failed -- Procesamiento processed_at TIMESTAMPTZ, dispatched_endpoints INTEGER DEFAULT 0, failed_endpoints INTEGER DEFAULT 0, -- Deduplicación idempotency_key VARCHAR(255), -- Metadata metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ ); -- ===================== -- TABLA: webhooks.subscriptions -- Suscripciones individuales evento-endpoint -- ===================== CREATE TABLE IF NOT EXISTS webhooks.subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint_id UUID NOT NULL REFERENCES webhooks.endpoints(id) ON DELETE CASCADE, event_type_id UUID NOT NULL REFERENCES webhooks.event_types(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Filtros específicos para esta suscripción filters JSONB DEFAULT '{}', -- Transformación del payload payload_template JSONB, -- Template para transformar el payload -- Estado is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, UNIQUE(endpoint_id, event_type_id) ); -- ===================== -- TABLA: webhooks.endpoint_logs -- Logs de actividad de endpoints -- ===================== CREATE TABLE IF NOT EXISTS webhooks.endpoint_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint_id UUID NOT NULL REFERENCES webhooks.endpoints(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Tipo de log log_type VARCHAR(30) NOT NULL, -- config_changed, activated, deactivated, verified, error, rate_limited -- Detalles message TEXT, details JSONB DEFAULT '{}', -- Actor actor_id UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- INDICES -- ===================== -- Indices para event_types CREATE INDEX IF NOT EXISTS idx_event_types_code ON webhooks.event_types(code); CREATE INDEX IF NOT EXISTS idx_event_types_category ON webhooks.event_types(category); CREATE INDEX IF NOT EXISTS idx_event_types_active ON webhooks.event_types(is_active) WHERE is_active = TRUE; -- Indices para endpoints CREATE INDEX IF NOT EXISTS idx_endpoints_tenant ON webhooks.endpoints(tenant_id); CREATE INDEX IF NOT EXISTS idx_endpoints_active ON webhooks.endpoints(is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_endpoints_events ON webhooks.endpoints USING GIN(subscribed_events); -- Indices para deliveries CREATE INDEX IF NOT EXISTS idx_deliveries_endpoint ON webhooks.deliveries(endpoint_id); CREATE INDEX IF NOT EXISTS idx_deliveries_tenant ON webhooks.deliveries(tenant_id); CREATE INDEX IF NOT EXISTS idx_deliveries_event ON webhooks.deliveries(event_type, event_id); CREATE INDEX IF NOT EXISTS idx_deliveries_status ON webhooks.deliveries(status); CREATE INDEX IF NOT EXISTS idx_deliveries_pending ON webhooks.deliveries(status, next_retry_at) WHERE status IN ('pending', 'retrying'); CREATE INDEX IF NOT EXISTS idx_deliveries_created ON webhooks.deliveries(created_at DESC); -- Indices para events CREATE INDEX IF NOT EXISTS idx_events_tenant ON webhooks.events(tenant_id); CREATE INDEX IF NOT EXISTS idx_events_type ON webhooks.events(event_type); CREATE INDEX IF NOT EXISTS idx_events_status ON webhooks.events(status); CREATE INDEX IF NOT EXISTS idx_events_pending ON webhooks.events(status, created_at) WHERE status = 'pending'; CREATE INDEX IF NOT EXISTS idx_events_idempotency ON webhooks.events(idempotency_key) WHERE idempotency_key IS NOT NULL; -- Indices para subscriptions CREATE INDEX IF NOT EXISTS idx_subs_endpoint ON webhooks.subscriptions(endpoint_id); CREATE INDEX IF NOT EXISTS idx_subs_event_type ON webhooks.subscriptions(event_type_id); CREATE INDEX IF NOT EXISTS idx_subs_tenant ON webhooks.subscriptions(tenant_id); -- Indices para endpoint_logs CREATE INDEX IF NOT EXISTS idx_endpoint_logs_endpoint ON webhooks.endpoint_logs(endpoint_id); CREATE INDEX IF NOT EXISTS idx_endpoint_logs_created ON webhooks.endpoint_logs(created_at DESC); -- ===================== -- RLS POLICIES -- ===================== -- Event types son globales (lectura pública) ALTER TABLE webhooks.event_types ENABLE ROW LEVEL SECURITY; CREATE POLICY public_read_event_types ON webhooks.event_types FOR SELECT USING (is_active = TRUE AND is_internal = FALSE); -- Endpoints por tenant ALTER TABLE webhooks.endpoints ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_endpoints ON webhooks.endpoints USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Deliveries por tenant ALTER TABLE webhooks.deliveries ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_deliveries ON webhooks.deliveries USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Events por tenant ALTER TABLE webhooks.events ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_events ON webhooks.events USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Subscriptions por tenant ALTER TABLE webhooks.subscriptions ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_subscriptions ON webhooks.subscriptions USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- Endpoint logs por tenant ALTER TABLE webhooks.endpoint_logs ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_endpoint_logs ON webhooks.endpoint_logs USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); -- ===================== -- FUNCIONES -- ===================== -- Función para generar signing secret CREATE OR REPLACE FUNCTION webhooks.generate_signing_secret() RETURNS VARCHAR(255) AS $$ BEGIN RETURN 'whsec_' || encode(gen_random_bytes(32), 'hex'); END; $$ LANGUAGE plpgsql; -- Función para crear un endpoint con secreto CREATE OR REPLACE FUNCTION webhooks.create_endpoint( p_tenant_id UUID, p_name VARCHAR(200), p_url TEXT, p_subscribed_events TEXT[], p_auth_type VARCHAR(30) DEFAULT 'none', p_auth_config JSONB DEFAULT '{}', p_created_by UUID DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_endpoint_id UUID; BEGIN INSERT INTO webhooks.endpoints ( tenant_id, name, url, subscribed_events, auth_type, auth_config, signing_secret, created_by ) VALUES ( p_tenant_id, p_name, p_url, p_subscribed_events, p_auth_type, p_auth_config, webhooks.generate_signing_secret(), p_created_by ) RETURNING id INTO v_endpoint_id; -- Log de creación INSERT INTO webhooks.endpoint_logs (endpoint_id, tenant_id, log_type, message, actor_id) VALUES (v_endpoint_id, p_tenant_id, 'created', 'Endpoint created', p_created_by); RETURN v_endpoint_id; END; $$ LANGUAGE plpgsql; -- Función para emitir un evento CREATE OR REPLACE FUNCTION webhooks.emit_event( p_tenant_id UUID, p_event_type VARCHAR(100), p_payload JSONB, p_resource_type VARCHAR(100) DEFAULT NULL, p_resource_id UUID DEFAULT NULL, p_triggered_by UUID DEFAULT NULL, p_idempotency_key VARCHAR(255) DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_event_id UUID; BEGIN -- Verificar deduplicación IF p_idempotency_key IS NOT NULL THEN SELECT id INTO v_event_id FROM webhooks.events WHERE tenant_id = p_tenant_id AND idempotency_key = p_idempotency_key AND created_at > CURRENT_TIMESTAMP - INTERVAL '24 hours'; IF FOUND THEN RETURN v_event_id; END IF; END IF; -- Crear evento INSERT INTO webhooks.events ( tenant_id, event_type, payload, resource_type, resource_id, triggered_by, idempotency_key, expires_at ) VALUES ( p_tenant_id, p_event_type, p_payload, p_resource_type, p_resource_id, p_triggered_by, p_idempotency_key, CURRENT_TIMESTAMP + INTERVAL '7 days' ) RETURNING id INTO v_event_id; RETURN v_event_id; END; $$ LANGUAGE plpgsql; -- Función para obtener endpoints suscritos a un evento CREATE OR REPLACE FUNCTION webhooks.get_subscribed_endpoints( p_tenant_id UUID, p_event_type VARCHAR(100) ) RETURNS TABLE ( endpoint_id UUID, url TEXT, auth_type VARCHAR(30), auth_config JSONB, custom_headers JSONB, signing_secret VARCHAR(255), timeout_seconds INTEGER, filters JSONB ) AS $$ BEGIN RETURN QUERY SELECT e.id as endpoint_id, e.url, e.auth_type, e.auth_config, e.custom_headers, e.signing_secret, e.timeout_seconds, e.filters FROM webhooks.endpoints e WHERE e.tenant_id = p_tenant_id AND e.is_active = TRUE AND p_event_type = ANY(e.subscribed_events); END; $$ LANGUAGE plpgsql STABLE; -- Función para encolar entrega CREATE OR REPLACE FUNCTION webhooks.queue_delivery( p_endpoint_id UUID, p_event_type VARCHAR(100), p_event_id UUID, p_payload JSONB ) RETURNS UUID AS $$ DECLARE v_endpoint RECORD; v_delivery_id UUID; BEGIN -- Obtener endpoint SELECT * INTO v_endpoint FROM webhooks.endpoints WHERE id = p_endpoint_id; IF NOT FOUND THEN RAISE EXCEPTION 'Endpoint not found: %', p_endpoint_id; END IF; -- Crear delivery INSERT INTO webhooks.deliveries ( endpoint_id, tenant_id, event_type, event_id, payload, payload_hash, request_url, request_method, request_headers, max_attempts, status, scheduled_at ) VALUES ( p_endpoint_id, v_endpoint.tenant_id, p_event_type, p_event_id, p_payload, encode(sha256(p_payload::text::bytea), 'hex'), v_endpoint.url, v_endpoint.http_method, v_endpoint.custom_headers, v_endpoint.max_retries + 1, 'pending', CURRENT_TIMESTAMP ) RETURNING id INTO v_delivery_id; RETURN v_delivery_id; END; $$ LANGUAGE plpgsql; -- Función para marcar entrega como completada CREATE OR REPLACE FUNCTION webhooks.mark_delivery_completed( p_delivery_id UUID, p_response_status INTEGER, p_response_headers JSONB, p_response_body TEXT, p_response_time_ms INTEGER ) RETURNS BOOLEAN AS $$ DECLARE v_delivery RECORD; v_is_success BOOLEAN; BEGIN SELECT * INTO v_delivery FROM webhooks.deliveries WHERE id = p_delivery_id; IF NOT FOUND THEN RETURN FALSE; END IF; v_is_success := p_response_status >= 200 AND p_response_status < 300; UPDATE webhooks.deliveries SET status = CASE WHEN v_is_success THEN 'delivered' ELSE 'failed' END, response_status = p_response_status, response_headers = p_response_headers, response_body = LEFT(p_response_body, 10000), -- Truncar respuesta larga response_time_ms = p_response_time_ms, completed_at = CURRENT_TIMESTAMP WHERE id = p_delivery_id; -- Actualizar estadísticas del endpoint UPDATE webhooks.endpoints SET total_deliveries = total_deliveries + 1, successful_deliveries = successful_deliveries + CASE WHEN v_is_success THEN 1 ELSE 0 END, failed_deliveries = failed_deliveries + CASE WHEN v_is_success THEN 0 ELSE 1 END, last_delivery_at = CURRENT_TIMESTAMP, last_success_at = CASE WHEN v_is_success THEN CURRENT_TIMESTAMP ELSE last_success_at END, last_failure_at = CASE WHEN v_is_success THEN last_failure_at ELSE CURRENT_TIMESTAMP END, updated_at = CURRENT_TIMESTAMP WHERE id = v_delivery.endpoint_id; RETURN v_is_success; END; $$ LANGUAGE plpgsql; -- Función para programar reintento CREATE OR REPLACE FUNCTION webhooks.schedule_retry( p_delivery_id UUID, p_error_message TEXT DEFAULT NULL ) RETURNS BOOLEAN AS $$ DECLARE v_delivery RECORD; v_endpoint RECORD; v_delay_seconds INTEGER; BEGIN SELECT d.*, e.retry_delay_seconds, e.retry_backoff_multiplier INTO v_delivery FROM webhooks.deliveries d JOIN webhooks.endpoints e ON e.id = d.endpoint_id WHERE d.id = p_delivery_id; IF NOT FOUND THEN RETURN FALSE; END IF; -- Verificar si quedan reintentos IF v_delivery.attempt_number >= v_delivery.max_attempts THEN UPDATE webhooks.deliveries SET status = 'failed', error_message = p_error_message, completed_at = CURRENT_TIMESTAMP WHERE id = p_delivery_id; RETURN FALSE; END IF; -- Calcular delay con backoff exponencial v_delay_seconds := v_delivery.retry_delay_seconds * POWER(v_delivery.retry_backoff_multiplier, v_delivery.attempt_number - 1); -- Programar reintento UPDATE webhooks.deliveries SET status = 'retrying', attempt_number = attempt_number + 1, next_retry_at = CURRENT_TIMESTAMP + (v_delay_seconds || ' seconds')::INTERVAL, error_message = p_error_message WHERE id = p_delivery_id; RETURN TRUE; END; $$ LANGUAGE plpgsql; -- Función para obtener estadísticas de un endpoint CREATE OR REPLACE FUNCTION webhooks.get_endpoint_stats( p_endpoint_id UUID, p_days INTEGER DEFAULT 7 ) RETURNS TABLE ( total_deliveries BIGINT, successful BIGINT, failed BIGINT, success_rate DECIMAL, avg_response_time_ms DECIMAL, deliveries_by_day JSONB, errors_by_type JSONB ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*) as total_deliveries, COUNT(*) FILTER (WHERE d.status = 'delivered') as successful, COUNT(*) FILTER (WHERE d.status = 'failed') as failed, ROUND( COUNT(*) FILTER (WHERE d.status = 'delivered')::DECIMAL / NULLIF(COUNT(*), 0) * 100, 2 ) as success_rate, ROUND(AVG(d.response_time_ms)::DECIMAL, 2) as avg_response_time_ms, jsonb_object_agg( COALESCE(DATE(d.created_at)::TEXT, 'unknown'), day_count ) as deliveries_by_day, jsonb_object_agg( COALESCE(d.error_code, 'unknown'), error_count ) FILTER (WHERE d.error_code IS NOT NULL) as errors_by_type FROM webhooks.deliveries d LEFT JOIN ( SELECT DATE(created_at) as day, COUNT(*) as day_count FROM webhooks.deliveries WHERE endpoint_id = p_endpoint_id AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY DATE(created_at) ) days ON TRUE LEFT JOIN ( SELECT error_code, COUNT(*) as error_count FROM webhooks.deliveries WHERE endpoint_id = p_endpoint_id AND error_code IS NOT NULL AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY error_code ) errors ON TRUE WHERE d.endpoint_id = p_endpoint_id AND d.created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL; END; $$ LANGUAGE plpgsql STABLE; -- Función para limpiar entregas antiguas CREATE OR REPLACE FUNCTION webhooks.cleanup_old_deliveries(p_days INTEGER DEFAULT 30) RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN DELETE FROM webhooks.deliveries WHERE created_at < CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL AND status IN ('delivered', 'failed', 'cancelled'); GET DIAGNOSTICS deleted_count = ROW_COUNT; -- También limpiar eventos procesados DELETE FROM webhooks.events WHERE created_at < CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL AND status = 'dispatched'; RETURN deleted_count; END; $$ LANGUAGE plpgsql; -- ===================== -- TRIGGERS -- ===================== CREATE OR REPLACE FUNCTION webhooks.update_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_event_types_updated_at BEFORE UPDATE ON webhooks.event_types FOR EACH ROW EXECUTE FUNCTION webhooks.update_timestamp(); CREATE TRIGGER trg_endpoints_updated_at BEFORE UPDATE ON webhooks.endpoints FOR EACH ROW EXECUTE FUNCTION webhooks.update_timestamp(); CREATE TRIGGER trg_subscriptions_updated_at BEFORE UPDATE ON webhooks.subscriptions FOR EACH ROW EXECUTE FUNCTION webhooks.update_timestamp(); -- ===================== -- SEED DATA: Event Types -- ===================== INSERT INTO webhooks.event_types (code, name, category, description, payload_schema) VALUES -- Sales events ('sale.created', 'Venta Creada', 'sales', 'Se creó una nueva venta', '{"type": "object", "properties": {"sale_id": {"type": "string"}, "total": {"type": "number"}}}'), ('sale.completed', 'Venta Completada', 'sales', 'Una venta fue completada y pagada', '{}'), ('sale.cancelled', 'Venta Cancelada', 'sales', 'Una venta fue cancelada', '{}'), ('sale.refunded', 'Venta Reembolsada', 'sales', 'Se procesó un reembolso', '{}'), -- Inventory events ('inventory.low_stock', 'Stock Bajo', 'inventory', 'Un producto alcanzó el nivel mínimo de stock', '{}'), ('inventory.out_of_stock', 'Sin Stock', 'inventory', 'Un producto se quedó sin stock', '{}'), ('inventory.adjusted', 'Inventario Ajustado', 'inventory', 'Se realizó un ajuste de inventario', '{}'), ('inventory.received', 'Mercancía Recibida', 'inventory', 'Se recibió mercancía en el almacén', '{}'), -- Customer events ('customer.created', 'Cliente Creado', 'customers', 'Se registró un nuevo cliente', '{}'), ('customer.updated', 'Cliente Actualizado', 'customers', 'Se actualizó información del cliente', '{}'), -- Auth events ('user.created', 'Usuario Creado', 'auth', 'Se creó un nuevo usuario', '{}'), ('user.login', 'Inicio de Sesión', 'auth', 'Un usuario inició sesión', '{}'), ('user.password_reset', 'Contraseña Restablecida', 'auth', 'Un usuario restableció su contraseña', '{}'), -- Billing events ('subscription.created', 'Suscripción Creada', 'billing', 'Se creó una nueva suscripción', '{}'), ('subscription.renewed', 'Suscripción Renovada', 'billing', 'Se renovó una suscripción', '{}'), ('subscription.cancelled', 'Suscripción Cancelada', 'billing', 'Se canceló una suscripción', '{}'), ('invoice.created', 'Factura Creada', 'billing', 'Se generó una nueva factura', '{}'), ('invoice.paid', 'Factura Pagada', 'billing', 'Se pagó una factura', '{}'), ('payment.received', 'Pago Recibido', 'billing', 'Se recibió un pago', '{}'), ('payment.failed', 'Pago Fallido', 'billing', 'Un pago falló', '{}'), -- System events ('system.maintenance', 'Mantenimiento Programado', 'system', 'Se programó mantenimiento del sistema', '{}'), ('system.alert', 'Alerta del Sistema', 'system', 'Se generó una alerta del sistema', '{}') ON CONFLICT (code) DO NOTHING; -- ===================== -- COMENTARIOS -- ===================== COMMENT ON TABLE webhooks.event_types IS 'Tipos de eventos disponibles para webhooks'; COMMENT ON TABLE webhooks.endpoints IS 'Endpoints configurados por tenant para recibir webhooks'; COMMENT ON TABLE webhooks.deliveries IS 'Log de entregas de webhooks con estado y reintentos'; COMMENT ON TABLE webhooks.events IS 'Cola de eventos pendientes de despacho'; COMMENT ON TABLE webhooks.subscriptions IS 'Suscripciones individuales evento-endpoint'; COMMENT ON TABLE webhooks.endpoint_logs IS 'Logs de actividad de endpoints'; COMMENT ON FUNCTION webhooks.emit_event IS 'Emite un evento a la cola de webhooks'; COMMENT ON FUNCTION webhooks.queue_delivery IS 'Encola una entrega de webhook'; COMMENT ON FUNCTION webhooks.mark_delivery_completed IS 'Marca una entrega como completada'; COMMENT ON FUNCTION webhooks.schedule_retry IS 'Programa un reintento de entrega'; COMMENT ON FUNCTION webhooks.get_endpoint_stats IS 'Obtiene estadísticas de un endpoint';