-- ============================================================= -- ARCHIVO: 02-payment-terminals.sql -- DESCRIPCION: Terminales de pago TPV (MercadoPago, Clip) -- VERSION: 1.0.0 -- PROYECTO: ERP-Construccion -- FECHA: 2026-01-25 -- PROPAGADO DESDE: erp-core v1.5.0 (PROP-CORE-004) -- ============================================================= -- Uso: Anticipos de obra, pagos parciales, materiales 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 - Construccion'; -- ============================================================= -- FIN DEL ARCHIVO -- =============================================================