- Replace old DDL structure with new numbered files (01-24) - Update migrations and seeds for new schema - Clean up deprecated files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
725 lines
24 KiB
PL/PgSQL
725 lines
24 KiB
PL/PgSQL
-- =============================================================
|
|
-- 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';
|