[TASK-2026-01-25-ERP-INTEGRACIONES] feat: Add payment terminals DDL
- Add schema payment_terminals with 5 tables - tenant_terminal_configs: Multi-tenant credentials (MercadoPago, Clip) - terminal_payments: Transaction records - terminal_webhook_events: Webhook event log - terminal_retry_queue: Retry queue with exponential backoff - terminal_payment_links: Payment links - RLS policies for tenant isolation - Helper functions for retry calculation and expiration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bc15eec17d
commit
32ac439c76
442
ddl/25-payment-terminals.sql
Normal file
442
ddl/25-payment-terminals.sql
Normal file
@ -0,0 +1,442 @@
|
||||
-- =============================================================
|
||||
-- ARCHIVO: 25-payment-terminals.sql
|
||||
-- DESCRIPCION: Terminales de pago TPV (MercadoPago, Clip)
|
||||
-- VERSION: 1.0.0
|
||||
-- PROYECTO: ERP-Core V2
|
||||
-- FECHA: 2026-01-25
|
||||
-- =============================================================
|
||||
-- Basado en: michangarrito INT-004 (MercadoPago) e INT-005 (Clip)
|
||||
-- =============================================================
|
||||
|
||||
-- =====================
|
||||
-- SCHEMA: payment_terminals
|
||||
-- =====================
|
||||
CREATE SCHEMA IF NOT EXISTS payment_terminals;
|
||||
|
||||
-- =====================
|
||||
-- ENUMS
|
||||
-- =====================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'terminal_provider') THEN
|
||||
CREATE TYPE payment_terminals.terminal_provider AS ENUM (
|
||||
'mercadopago',
|
||||
'clip',
|
||||
'stripe_terminal'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'terminal_payment_status') THEN
|
||||
CREATE TYPE payment_terminals.terminal_payment_status AS ENUM (
|
||||
'pending',
|
||||
'processing',
|
||||
'approved',
|
||||
'authorized',
|
||||
'in_process',
|
||||
'rejected',
|
||||
'refunded',
|
||||
'partially_refunded',
|
||||
'cancelled',
|
||||
'charged_back'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_method_type') THEN
|
||||
CREATE TYPE payment_terminals.payment_method_type AS ENUM (
|
||||
'card',
|
||||
'qr',
|
||||
'link',
|
||||
'cash',
|
||||
'bank_transfer'
|
||||
);
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
-- =====================
|
||||
-- TABLA: tenant_terminal_configs
|
||||
-- Credenciales de proveedores por tenant (no por branch)
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS payment_terminals.tenant_terminal_configs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Proveedor
|
||||
provider payment_terminals.terminal_provider NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Credenciales (encriptadas AES-256)
|
||||
credentials JSONB NOT NULL DEFAULT '{}',
|
||||
-- MercadoPago: { access_token, public_key, collector_id }
|
||||
-- Clip: { api_key, secret_key, merchant_id }
|
||||
|
||||
-- Configuración específica del proveedor
|
||||
config JSONB DEFAULT '{}',
|
||||
-- MercadoPago: { statement_descriptor, notification_url }
|
||||
-- Clip: { default_currency, webhook_secret }
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_verified BOOLEAN DEFAULT false,
|
||||
verification_error TEXT,
|
||||
verified_at TIMESTAMPTZ,
|
||||
|
||||
-- Límites
|
||||
daily_limit DECIMAL(12,2),
|
||||
monthly_limit DECIMAL(12,2),
|
||||
transaction_limit DECIMAL(12,2),
|
||||
|
||||
-- Webhook
|
||||
webhook_url VARCHAR(500),
|
||||
webhook_secret VARCHAR(255),
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
|
||||
UNIQUE(tenant_id, provider, name)
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: terminal_payments
|
||||
-- Transacciones procesadas por terminales TPV
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS payment_terminals.terminal_payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
|
||||
-- Configuración usada
|
||||
config_id UUID REFERENCES payment_terminals.tenant_terminal_configs(id),
|
||||
branch_terminal_id UUID REFERENCES core.branch_payment_terminals(id),
|
||||
|
||||
-- Proveedor y referencia externa
|
||||
provider payment_terminals.terminal_provider NOT NULL,
|
||||
external_id VARCHAR(255),
|
||||
external_status VARCHAR(50),
|
||||
|
||||
-- Monto
|
||||
amount DECIMAL(12,2) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'MXN',
|
||||
|
||||
-- Estado
|
||||
status payment_terminals.terminal_payment_status DEFAULT 'pending',
|
||||
|
||||
-- Método de pago
|
||||
payment_method payment_terminals.payment_method_type DEFAULT 'card',
|
||||
|
||||
-- Datos de tarjeta (parciales, no sensibles)
|
||||
card_last_four VARCHAR(4),
|
||||
card_brand VARCHAR(20),
|
||||
card_type VARCHAR(20), -- credit, debit, prepaid
|
||||
|
||||
-- Cliente
|
||||
customer_email VARCHAR(255),
|
||||
customer_phone VARCHAR(20),
|
||||
customer_name VARCHAR(200),
|
||||
|
||||
-- Descripción
|
||||
description TEXT,
|
||||
statement_descriptor VARCHAR(50),
|
||||
|
||||
-- Referencia interna (orden, venta, etc.)
|
||||
reference_type VARCHAR(50), -- sale, order, invoice
|
||||
reference_id UUID,
|
||||
|
||||
-- Comisiones
|
||||
fee_amount DECIMAL(10,4),
|
||||
fee_details JSONB,
|
||||
net_amount DECIMAL(12,2),
|
||||
|
||||
-- Reembolso
|
||||
refunded_amount DECIMAL(12,2) DEFAULT 0,
|
||||
refund_reason TEXT,
|
||||
|
||||
-- Respuesta del proveedor
|
||||
provider_response JSONB,
|
||||
|
||||
-- Error
|
||||
error_code VARCHAR(50),
|
||||
error_message TEXT,
|
||||
|
||||
-- Timestamps
|
||||
processed_at TIMESTAMPTZ,
|
||||
refunded_at TIMESTAMPTZ,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: terminal_webhook_events
|
||||
-- Eventos de webhook recibidos
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS payment_terminals.terminal_webhook_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
|
||||
|
||||
-- Proveedor y evento
|
||||
provider payment_terminals.terminal_provider NOT NULL,
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
event_id VARCHAR(255), -- ID del evento en el proveedor
|
||||
|
||||
-- Referencia
|
||||
payment_id UUID REFERENCES payment_terminals.terminal_payments(id),
|
||||
external_id VARCHAR(255),
|
||||
|
||||
-- Payload completo
|
||||
payload JSONB NOT NULL,
|
||||
headers JSONB,
|
||||
|
||||
-- Validación de firma
|
||||
signature_valid BOOLEAN,
|
||||
|
||||
-- Procesamiento
|
||||
processed BOOLEAN DEFAULT false,
|
||||
processed_at TIMESTAMPTZ,
|
||||
processing_error TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
|
||||
-- Idempotencia
|
||||
idempotency_key VARCHAR(255),
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(provider, event_id)
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: terminal_retry_queue
|
||||
-- Cola de reintentos para operaciones fallidas
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS payment_terminals.terminal_retry_queue (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
|
||||
|
||||
-- Referencia
|
||||
payment_id UUID REFERENCES payment_terminals.terminal_payments(id),
|
||||
provider payment_terminals.terminal_provider NOT NULL,
|
||||
|
||||
-- Operación
|
||||
operation VARCHAR(50) NOT NULL, -- create_payment, refund, check_status
|
||||
payload JSONB NOT NULL,
|
||||
|
||||
-- Reintentos
|
||||
attempts INTEGER DEFAULT 0,
|
||||
max_attempts INTEGER DEFAULT 5,
|
||||
next_retry_at TIMESTAMPTZ,
|
||||
|
||||
-- Error
|
||||
last_error TEXT,
|
||||
last_error_code VARCHAR(50),
|
||||
|
||||
-- Estado
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed
|
||||
completed_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- TABLA: terminal_payment_links
|
||||
-- Links de pago generados
|
||||
-- =====================
|
||||
CREATE TABLE IF NOT EXISTS payment_terminals.terminal_payment_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||
config_id UUID REFERENCES payment_terminals.tenant_terminal_configs(id),
|
||||
|
||||
-- Proveedor y referencia
|
||||
provider payment_terminals.terminal_provider NOT NULL,
|
||||
external_id VARCHAR(255),
|
||||
|
||||
-- URL del link
|
||||
url VARCHAR(1000) NOT NULL,
|
||||
short_url VARCHAR(500),
|
||||
|
||||
-- Monto
|
||||
amount DECIMAL(12,2) NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'MXN',
|
||||
|
||||
-- Descripción
|
||||
title VARCHAR(200),
|
||||
description TEXT,
|
||||
|
||||
-- Estado
|
||||
status VARCHAR(30) DEFAULT 'active', -- active, paid, expired, cancelled
|
||||
|
||||
-- Pago asociado (cuando se paga)
|
||||
payment_id UUID REFERENCES payment_terminals.terminal_payments(id),
|
||||
|
||||
-- Expiración
|
||||
expires_at TIMESTAMPTZ,
|
||||
|
||||
-- Referencia interna
|
||||
reference_type VARCHAR(50),
|
||||
reference_id UUID,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}',
|
||||
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- =====================
|
||||
-- INDICES
|
||||
-- =====================
|
||||
|
||||
-- tenant_terminal_configs
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_configs_tenant
|
||||
ON payment_terminals.tenant_terminal_configs(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_configs_provider
|
||||
ON payment_terminals.tenant_terminal_configs(provider);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_configs_active
|
||||
ON payment_terminals.tenant_terminal_configs(is_active) WHERE is_active = true;
|
||||
|
||||
-- terminal_payments
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_tenant
|
||||
ON payment_terminals.terminal_payments(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_provider
|
||||
ON payment_terminals.terminal_payments(provider);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_status
|
||||
ON payment_terminals.terminal_payments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_external
|
||||
ON payment_terminals.terminal_payments(external_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_reference
|
||||
ON payment_terminals.terminal_payments(reference_type, reference_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_terminal_payments_created
|
||||
ON payment_terminals.terminal_payments(created_at DESC);
|
||||
|
||||
-- terminal_webhook_events
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_tenant
|
||||
ON payment_terminals.terminal_webhook_events(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_provider
|
||||
ON payment_terminals.terminal_webhook_events(provider);
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_processed
|
||||
ON payment_terminals.terminal_webhook_events(processed, created_at)
|
||||
WHERE processed = false;
|
||||
CREATE INDEX IF NOT EXISTS idx_webhook_events_idempotency
|
||||
ON payment_terminals.terminal_webhook_events(idempotency_key);
|
||||
|
||||
-- terminal_retry_queue
|
||||
CREATE INDEX IF NOT EXISTS idx_retry_queue_next
|
||||
ON payment_terminals.terminal_retry_queue(next_retry_at)
|
||||
WHERE status = 'pending';
|
||||
CREATE INDEX IF NOT EXISTS idx_retry_queue_tenant
|
||||
ON payment_terminals.terminal_retry_queue(tenant_id);
|
||||
|
||||
-- terminal_payment_links
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_links_tenant
|
||||
ON payment_terminals.terminal_payment_links(tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_links_status
|
||||
ON payment_terminals.terminal_payment_links(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_links_expires
|
||||
ON payment_terminals.terminal_payment_links(expires_at)
|
||||
WHERE status = 'active';
|
||||
|
||||
-- =====================
|
||||
-- RLS POLICIES
|
||||
-- =====================
|
||||
|
||||
ALTER TABLE payment_terminals.tenant_terminal_configs ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payment_terminals.terminal_payments ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payment_terminals.terminal_webhook_events ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payment_terminals.terminal_retry_queue ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE payment_terminals.terminal_payment_links ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Políticas de aislamiento por tenant
|
||||
CREATE POLICY tenant_configs_isolation ON payment_terminals.tenant_terminal_configs
|
||||
USING (tenant_id = current_setting('app.current_tenant', true)::UUID);
|
||||
|
||||
CREATE POLICY terminal_payments_isolation ON payment_terminals.terminal_payments
|
||||
USING (tenant_id = current_setting('app.current_tenant', true)::UUID);
|
||||
|
||||
CREATE POLICY webhook_events_isolation ON payment_terminals.terminal_webhook_events
|
||||
USING (tenant_id = current_setting('app.current_tenant', true)::UUID);
|
||||
|
||||
CREATE POLICY retry_queue_isolation ON payment_terminals.terminal_retry_queue
|
||||
USING (tenant_id = current_setting('app.current_tenant', true)::UUID);
|
||||
|
||||
CREATE POLICY payment_links_isolation ON payment_terminals.terminal_payment_links
|
||||
USING (tenant_id = current_setting('app.current_tenant', true)::UUID);
|
||||
|
||||
-- =====================
|
||||
-- FUNCIONES
|
||||
-- =====================
|
||||
|
||||
-- Función para calcular siguiente retry con backoff exponencial
|
||||
CREATE OR REPLACE FUNCTION payment_terminals.calculate_next_retry(
|
||||
p_attempts INTEGER,
|
||||
p_base_delay_seconds INTEGER DEFAULT 60
|
||||
) RETURNS TIMESTAMPTZ AS $$
|
||||
BEGIN
|
||||
-- Backoff exponencial: base * 2^attempts
|
||||
-- Ejemplo con base 60s: 60s, 120s, 240s, 480s, 960s
|
||||
RETURN NOW() + (p_base_delay_seconds * POWER(2, p_attempts)) * INTERVAL '1 second';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Función para marcar pagos expirados
|
||||
CREATE OR REPLACE FUNCTION payment_terminals.expire_old_pending_payments()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
affected_rows INTEGER;
|
||||
BEGIN
|
||||
UPDATE payment_terminals.terminal_payments
|
||||
SET status = 'cancelled',
|
||||
updated_at = NOW(),
|
||||
error_message = 'Payment expired'
|
||||
WHERE status = 'pending'
|
||||
AND created_at < NOW() - INTERVAL '24 hours';
|
||||
|
||||
GET DIAGNOSTICS affected_rows = ROW_COUNT;
|
||||
RETURN affected_rows;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Función para expirar links de pago
|
||||
CREATE OR REPLACE FUNCTION payment_terminals.expire_payment_links()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
affected_rows INTEGER;
|
||||
BEGIN
|
||||
UPDATE payment_terminals.terminal_payment_links
|
||||
SET status = 'expired',
|
||||
updated_at = NOW()
|
||||
WHERE status = 'active'
|
||||
AND expires_at IS NOT NULL
|
||||
AND expires_at < NOW();
|
||||
|
||||
GET DIAGNOSTICS affected_rows = ROW_COUNT;
|
||||
RETURN affected_rows;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- =====================
|
||||
-- COMENTARIOS
|
||||
-- =====================
|
||||
|
||||
COMMENT ON SCHEMA payment_terminals IS 'Terminales de pago TPV (MercadoPago, Clip, Stripe Terminal)';
|
||||
COMMENT ON TABLE payment_terminals.tenant_terminal_configs IS 'Configuración de credenciales de proveedores TPV por tenant';
|
||||
COMMENT ON TABLE payment_terminals.terminal_payments IS 'Transacciones procesadas por terminales TPV';
|
||||
COMMENT ON TABLE payment_terminals.terminal_webhook_events IS 'Eventos de webhook recibidos de proveedores TPV';
|
||||
COMMENT ON TABLE payment_terminals.terminal_retry_queue IS 'Cola de reintentos para operaciones fallidas';
|
||||
COMMENT ON TABLE payment_terminals.terminal_payment_links IS 'Links de pago generados';
|
||||
|
||||
-- =============================================================
|
||||
-- FIN DEL ARCHIVO
|
||||
-- =============================================================
|
||||
Loading…
Reference in New Issue
Block a user