From 5cf220326a9e2291ac264b7b783d564d04073e80 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 02:47:05 -0600 Subject: [PATCH] [PROP-CORE-004] feat: Add payment_terminals schema DDL Propagated from erp-core v1.5.0 Use: Cobro de servicios, venta refacciones mostrador Co-Authored-By: Claude Opus 4.5 --- init/12-payment-terminals.sql | 275 ++++++++++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 init/12-payment-terminals.sql diff --git a/init/12-payment-terminals.sql b/init/12-payment-terminals.sql new file mode 100644 index 0000000..5c5ba2b --- /dev/null +++ b/init/12-payment-terminals.sql @@ -0,0 +1,275 @@ +-- ============================================================= +-- ARCHIVO: 12-payment-terminals.sql +-- DESCRIPCION: Terminales de pago TPV (MercadoPago, Clip) +-- VERSION: 1.0.0 +-- PROYECTO: ERP-Mecanicas-Diesel +-- FECHA: 2026-01-25 +-- PROPAGADO DESDE: erp-core v1.5.0 (PROP-CORE-004) +-- ============================================================= +-- Uso: Cobro de servicios, venta refacciones mostrador +-- ============================================================= + +-- ===================== +-- 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 +-- ===================== +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, + provider payment_terminals.terminal_provider NOT NULL, + name VARCHAR(100) NOT NULL, + credentials JSONB NOT NULL DEFAULT '{}', + config JSONB DEFAULT '{}', + is_active BOOLEAN DEFAULT true, + is_verified BOOLEAN DEFAULT false, + verification_error TEXT, + verified_at TIMESTAMPTZ, + daily_limit DECIMAL(12,2), + monthly_limit DECIMAL(12,2), + transaction_limit DECIMAL(12,2), + webhook_url VARCHAR(500), + webhook_secret VARCHAR(255), + 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 +-- ===================== +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, + config_id UUID REFERENCES payment_terminals.tenant_terminal_configs(id), + branch_terminal_id UUID, + provider payment_terminals.terminal_provider NOT NULL, + external_id VARCHAR(255), + external_status VARCHAR(50), + amount DECIMAL(12,2) NOT NULL, + currency VARCHAR(3) DEFAULT 'MXN', + status payment_terminals.terminal_payment_status DEFAULT 'pending', + payment_method payment_terminals.payment_method_type DEFAULT 'card', + card_last_four VARCHAR(4), + card_brand VARCHAR(20), + card_type VARCHAR(20), + customer_email VARCHAR(255), + customer_phone VARCHAR(20), + customer_name VARCHAR(200), + description TEXT, + statement_descriptor VARCHAR(50), + reference_type VARCHAR(50), + reference_id UUID, + fee_amount DECIMAL(10,4), + fee_details JSONB, + net_amount DECIMAL(12,2), + refunded_amount DECIMAL(12,2) DEFAULT 0, + refund_reason TEXT, + provider_response JSONB, + error_code VARCHAR(50), + error_message TEXT, + processed_at TIMESTAMPTZ, + refunded_at TIMESTAMPTZ, + 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 +-- ===================== +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), + provider payment_terminals.terminal_provider NOT NULL, + event_type VARCHAR(100) NOT NULL, + event_id VARCHAR(255), + payment_id UUID REFERENCES payment_terminals.terminal_payments(id), + external_id VARCHAR(255), + payload JSONB NOT NULL, + headers JSONB, + signature_valid BOOLEAN, + processed BOOLEAN DEFAULT false, + processed_at TIMESTAMPTZ, + processing_error TEXT, + retry_count INTEGER DEFAULT 0, + idempotency_key VARCHAR(255), + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + UNIQUE(provider, event_id) +); + +-- ===================== +-- TABLA: terminal_retry_queue +-- ===================== +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), + payment_id UUID REFERENCES payment_terminals.terminal_payments(id), + provider payment_terminals.terminal_provider NOT NULL, + operation VARCHAR(50) NOT NULL, + payload JSONB NOT NULL, + attempts INTEGER DEFAULT 0, + max_attempts INTEGER DEFAULT 5, + next_retry_at TIMESTAMPTZ, + last_error TEXT, + last_error_code VARCHAR(50), + status VARCHAR(20) DEFAULT 'pending', + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP +); + +-- ===================== +-- TABLA: terminal_payment_links +-- ===================== +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), + provider payment_terminals.terminal_provider NOT NULL, + external_id VARCHAR(255), + url VARCHAR(1000) NOT NULL, + short_url VARCHAR(500), + amount DECIMAL(12,2) NOT NULL, + currency VARCHAR(3) DEFAULT 'MXN', + title VARCHAR(200), + description TEXT, + status VARCHAR(30) DEFAULT 'active', + payment_id UUID REFERENCES payment_terminals.terminal_payments(id), + expires_at TIMESTAMPTZ, + reference_type VARCHAR(50), + reference_id UUID, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + created_by UUID REFERENCES auth.users(id) +); + +-- ===================== +-- INDICES +-- ===================== +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_payments_tenant ON payment_terminals.terminal_payments(tenant_id); +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_webhook_events_tenant ON payment_terminals.terminal_webhook_events(tenant_id); +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_retry_queue_next ON payment_terminals.terminal_retry_queue(next_retry_at) WHERE status = 'pending'; +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); + +-- ===================== +-- 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; + +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 +-- ===================== +CREATE OR REPLACE FUNCTION payment_terminals.calculate_next_retry( + p_attempts INTEGER, + p_base_delay_seconds INTEGER DEFAULT 60 +) RETURNS TIMESTAMPTZ AS $$ +BEGIN + RETURN NOW() + (p_base_delay_seconds * POWER(2, p_attempts)) * INTERVAL '1 second'; +END; +$$ LANGUAGE plpgsql; + +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; + +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; + +COMMENT ON SCHEMA payment_terminals IS 'Terminales de pago TPV - Mecanicas Diesel'; +-- ============================================================= +-- FIN DEL ARCHIVO +-- =============================================================