# DDL Specification: integrations Schema ## Identificacion | Campo | Valor | |-------|-------| | **Schema** | integrations | | **Modulo** | MGN-016 Integraciones de Pagos y POS | | **Version** | 1.0 | | **Fecha** | 2025-12-05 | | **Estado** | Ready | --- ## Diagrama ER ```mermaid erDiagram tenants ||--o{ payment_providers : has payment_providers ||--o| payment_credentials : has payment_providers ||--o{ payment_terminals : manages tenants ||--o{ payment_transactions : processes payment_transactions }o--|| payment_providers : via payment_transactions }o--|| payment_terminals : on tenants ||--o{ webhook_logs : receives tenants ||--o{ reconciliation_batches : runs payment_providers { uuid id PK uuid tenant_id FK string provider_type string name boolean is_active boolean is_sandbox } payment_credentials { uuid id PK uuid provider_id FK UK bytea access_token_encrypted bytea refresh_token_encrypted timestamp token_expires_at jsonb metadata } payment_terminals { uuid id PK uuid provider_id FK string terminal_id string name string status jsonb device_info boolean is_active } payment_transactions { uuid id PK uuid tenant_id FK uuid provider_id FK uuid terminal_id FK string external_id UK string order_id decimal amount string currency string status jsonb gateway_response } webhook_logs { uuid id PK uuid tenant_id FK uuid provider_id FK string event_type jsonb payload string status timestamp received_at } reconciliation_batches { uuid id PK uuid tenant_id FK uuid provider_id FK date batch_date int total_transactions decimal total_amount string status } ``` --- ## Tablas ### 1. payment_providers Proveedores de pago configurados por tenant. ```sql CREATE TABLE integrations.payment_providers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, -- Tipo de proveedor provider_type VARCHAR(30) NOT NULL, -- mercadopago, clip, stripe, conekta, openpay -- Identificacion name VARCHAR(100) NOT NULL, display_name VARCHAR(100), -- Configuracion settings JSONB NOT NULL DEFAULT '{}'::jsonb, -- Ejemplo: {"country": "MX", "public_key": "...", "webhook_secret": "..."} -- Estado is_active BOOLEAN NOT NULL DEFAULT true, is_sandbox BOOLEAN NOT NULL DEFAULT true, connection_status VARCHAR(20) NOT NULL DEFAULT 'disconnected', last_connected_at TIMESTAMPTZ, last_error VARCHAR(500), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, CONSTRAINT uq_payment_providers UNIQUE (tenant_id, provider_type, is_sandbox), CONSTRAINT chk_payment_providers_type CHECK (provider_type IN ( 'mercadopago', 'clip', 'stripe', 'conekta', 'openpay', 'paypal' )), CONSTRAINT chk_payment_providers_status CHECK (connection_status IN ( 'disconnected', 'connecting', 'connected', 'error', 'expired' )) ); -- Indices CREATE INDEX idx_payment_providers_tenant ON integrations.payment_providers(tenant_id); CREATE INDEX idx_payment_providers_type ON integrations.payment_providers(provider_type); CREATE INDEX idx_payment_providers_active ON integrations.payment_providers(tenant_id, is_active) WHERE is_active = true; -- Trigger para updated_at CREATE TRIGGER trg_payment_providers_updated_at BEFORE UPDATE ON integrations.payment_providers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- RLS ALTER TABLE integrations.payment_providers ENABLE ROW LEVEL SECURITY; CREATE POLICY payment_providers_tenant_isolation ON integrations.payment_providers FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid); -- Comentarios COMMENT ON TABLE integrations.payment_providers IS 'Proveedores de pago configurados por tenant (MGN-016)'; COMMENT ON COLUMN integrations.payment_providers.is_sandbox IS 'true para ambiente de pruebas'; ``` --- ### 2. payment_credentials Credenciales encriptadas de proveedores. ```sql CREATE TABLE integrations.payment_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), provider_id UUID NOT NULL UNIQUE REFERENCES integrations.payment_providers(id) ON DELETE CASCADE, -- OAuth tokens (encriptados con AES-256) access_token_encrypted BYTEA, refresh_token_encrypted BYTEA, token_type VARCHAR(20) DEFAULT 'Bearer', token_expires_at TIMESTAMPTZ, -- API Keys (encriptadas) api_key_encrypted BYTEA, api_secret_encrypted BYTEA, -- Metadata OAuth oauth_user_id VARCHAR(100), oauth_scopes TEXT[], -- Estado is_valid BOOLEAN NOT NULL DEFAULT true, last_refresh_at TIMESTAMPTZ, refresh_error VARCHAR(500), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Trigger para updated_at CREATE TRIGGER trg_payment_credentials_updated_at BEFORE UPDATE ON integrations.payment_credentials FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE integrations.payment_credentials IS 'Credenciales encriptadas de proveedores (PCI compliant)'; COMMENT ON COLUMN integrations.payment_credentials.access_token_encrypted IS 'Token encriptado con AES-256-GCM'; ``` --- ### 3. payment_terminals Terminales de pago fisicas. ```sql CREATE TABLE integrations.payment_terminals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), provider_id UUID NOT NULL REFERENCES integrations.payment_providers(id) ON DELETE CASCADE, -- Identificacion external_terminal_id VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL, serial_number VARCHAR(100), -- Tipo y modelo terminal_type VARCHAR(30) NOT NULL, -- point, point_pro, clip_plus, clip_pro, verifone_p400, bbpos_wise model VARCHAR(50), firmware_version VARCHAR(50), -- Ubicacion location_name VARCHAR(100), location_address VARCHAR(200), -- Estado status VARCHAR(20) NOT NULL DEFAULT 'offline', last_seen_at TIMESTAMPTZ, battery_level INT, -- 0-100 para terminales portatiles -- Configuracion settings JSONB DEFAULT '{}'::jsonb, -- Ejemplo: {"print_receipt": true, "timeout_seconds": 60} is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_payment_terminals UNIQUE (provider_id, external_terminal_id), CONSTRAINT chk_payment_terminals_type CHECK (terminal_type IN ( 'point', 'point_smart', 'point_pro', 'clip_plus', 'clip_pro', 'verifone_p400', 'bbpos_wise', 'stripe_reader' )), CONSTRAINT chk_payment_terminals_status CHECK (status IN ( 'online', 'offline', 'busy', 'error', 'maintenance' )) ); -- Indices CREATE INDEX idx_payment_terminals_provider ON integrations.payment_terminals(provider_id); CREATE INDEX idx_payment_terminals_status ON integrations.payment_terminals(status); CREATE INDEX idx_payment_terminals_active ON integrations.payment_terminals(provider_id, is_active) WHERE is_active = true; -- Trigger para updated_at CREATE TRIGGER trg_payment_terminals_updated_at BEFORE UPDATE ON integrations.payment_terminals FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE integrations.payment_terminals IS 'Terminales de pago fisicas (MGN-016)'; COMMENT ON COLUMN integrations.payment_terminals.external_terminal_id IS 'ID asignado por el proveedor'; ``` --- ### 4. payment_transactions Transacciones de pago procesadas. ```sql CREATE TABLE integrations.payment_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id), provider_id UUID NOT NULL REFERENCES integrations.payment_providers(id), terminal_id UUID REFERENCES integrations.payment_terminals(id), -- Identificadores external_id VARCHAR(100) NOT NULL, -- ID del gateway order_id VARCHAR(100), -- ID del pedido interno reference VARCHAR(100), -- Referencia para el cliente -- Monto amount DECIMAL(15,2) NOT NULL, currency VARCHAR(3) NOT NULL DEFAULT 'MXN', exchange_rate DECIMAL(10,6) DEFAULT 1.0, -- Metodo de pago payment_method VARCHAR(30) NOT NULL, -- card_present, card_not_present, oxxo, spei, mercado_credito card_brand VARCHAR(20), -- visa, mastercard, amex card_last4 VARCHAR(4), card_funding VARCHAR(20), -- credit, debit, prepaid installments INT DEFAULT 1, -- Estado status VARCHAR(20) NOT NULL DEFAULT 'pending', failure_code VARCHAR(50), failure_message VARCHAR(500), -- Respuesta del gateway gateway_response JSONB NOT NULL DEFAULT '{}'::jsonb, authorization_code VARCHAR(50), -- Metadata metadata JSONB DEFAULT '{}'::jsonb, -- Ejemplo: {"customer_email": "...", "description": "..."} -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, processed_at TIMESTAMPTZ, settled_at TIMESTAMPTZ, -- Conciliacion reconciliation_batch_id UUID, reconciled_at TIMESTAMPTZ, CONSTRAINT uq_payment_transactions UNIQUE (provider_id, external_id), CONSTRAINT chk_payment_transactions_status CHECK (status IN ( 'pending', 'processing', 'approved', 'rejected', 'cancelled', 'refunded', 'partially_refunded', 'chargebacked', 'expired' )), CONSTRAINT chk_payment_transactions_method CHECK (payment_method IN ( 'card_present', 'card_not_present', 'oxxo', 'spei', 'mercado_credito', 'paypal', 'bank_transfer' )), CONSTRAINT chk_payment_transactions_amount CHECK (amount > 0) ); -- Indices CREATE INDEX idx_payment_transactions_tenant ON integrations.payment_transactions(tenant_id); CREATE INDEX idx_payment_transactions_provider ON integrations.payment_transactions(provider_id); CREATE INDEX idx_payment_transactions_terminal ON integrations.payment_transactions(terminal_id); CREATE INDEX idx_payment_transactions_order ON integrations.payment_transactions(order_id) WHERE order_id IS NOT NULL; CREATE INDEX idx_payment_transactions_status ON integrations.payment_transactions(status); CREATE INDEX idx_payment_transactions_date ON integrations.payment_transactions(created_at DESC); CREATE INDEX idx_payment_transactions_reconciliation ON integrations.payment_transactions(reconciliation_batch_id) WHERE reconciliation_batch_id IS NOT NULL; -- RLS ALTER TABLE integrations.payment_transactions ENABLE ROW LEVEL SECURITY; CREATE POLICY payment_transactions_tenant_isolation ON integrations.payment_transactions FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid); -- Comentarios COMMENT ON TABLE integrations.payment_transactions IS 'Transacciones de cobro a clientes del tenant'; COMMENT ON COLUMN integrations.payment_transactions.order_id IS 'ID del pedido en el sistema (sales.orders)'; ``` --- ### 5. refunds Devoluciones de transacciones. ```sql CREATE TABLE integrations.refunds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), transaction_id UUID NOT NULL REFERENCES integrations.payment_transactions(id), -- Identificadores external_id VARCHAR(100) NOT NULL, reference VARCHAR(100), -- Monto amount DECIMAL(15,2) NOT NULL, currency VARCHAR(3) NOT NULL DEFAULT 'MXN', refund_type VARCHAR(20) NOT NULL DEFAULT 'full', -- Estado status VARCHAR(20) NOT NULL DEFAULT 'pending', failure_message VARCHAR(500), -- Motivo reason VARCHAR(50), notes VARCHAR(500), -- Gateway response gateway_response JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, processed_at TIMESTAMPTZ, created_by UUID, CONSTRAINT chk_refunds_type CHECK (refund_type IN ('full', 'partial')), CONSTRAINT chk_refunds_status CHECK (status IN ('pending', 'processing', 'approved', 'rejected')), CONSTRAINT chk_refunds_amount CHECK (amount > 0) ); -- Indices CREATE INDEX idx_refunds_transaction ON integrations.refunds(transaction_id); CREATE INDEX idx_refunds_status ON integrations.refunds(status); CREATE INDEX idx_refunds_date ON integrations.refunds(created_at DESC); -- Comentarios COMMENT ON TABLE integrations.refunds IS 'Devoluciones de transacciones'; ``` --- ### 6. webhook_logs Log de webhooks recibidos. ```sql CREATE TABLE integrations.webhook_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES core_tenants.tenants(id), provider_id UUID REFERENCES integrations.payment_providers(id), -- Evento event_type VARCHAR(100) NOT NULL, event_id VARCHAR(100), -- Payload payload JSONB NOT NULL, headers JSONB DEFAULT '{}'::jsonb, signature VARCHAR(500), -- Estado de procesamiento status VARCHAR(20) NOT NULL DEFAULT 'received', processed_at TIMESTAMPTZ, error_message VARCHAR(500), retry_count INT NOT NULL DEFAULT 0, -- Request info source_ip VARCHAR(45), received_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_webhook_logs_status CHECK (status IN ( 'received', 'processing', 'processed', 'failed', 'ignored' )) ); -- Indices CREATE INDEX idx_webhook_logs_tenant ON integrations.webhook_logs(tenant_id); CREATE INDEX idx_webhook_logs_provider ON integrations.webhook_logs(provider_id); CREATE INDEX idx_webhook_logs_event ON integrations.webhook_logs(event_type); CREATE INDEX idx_webhook_logs_status ON integrations.webhook_logs(status); CREATE INDEX idx_webhook_logs_date ON integrations.webhook_logs(received_at DESC); -- Particionamiento por mes (opcional, para alto volumen) -- CREATE TABLE integrations.webhook_logs_y2025m01 PARTITION OF integrations.webhook_logs -- FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); -- Comentarios COMMENT ON TABLE integrations.webhook_logs IS 'Log de webhooks de proveedores de pago'; COMMENT ON COLUMN integrations.webhook_logs.signature IS 'Firma HMAC para validacion'; ``` --- ### 7. reconciliation_batches Lotes de conciliacion. ```sql CREATE TABLE integrations.reconciliation_batches ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id), provider_id UUID NOT NULL REFERENCES integrations.payment_providers(id), -- Periodo batch_date DATE NOT NULL, period_start TIMESTAMPTZ NOT NULL, period_end TIMESTAMPTZ NOT NULL, -- Totales esperados (del gateway) expected_count INT NOT NULL DEFAULT 0, expected_amount DECIMAL(15,2) NOT NULL DEFAULT 0, -- Totales encontrados (en sistema) matched_count INT NOT NULL DEFAULT 0, matched_amount DECIMAL(15,2) NOT NULL DEFAULT 0, -- Diferencias unmatched_count INT NOT NULL DEFAULT 0, difference_amount DECIMAL(15,2) NOT NULL DEFAULT 0, -- Estado status VARCHAR(20) NOT NULL DEFAULT 'pending', completed_at TIMESTAMPTZ, -- Detalle de diferencias discrepancies JSONB DEFAULT '[]'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, CONSTRAINT uq_reconciliation_batches UNIQUE (tenant_id, provider_id, batch_date), CONSTRAINT chk_reconciliation_status CHECK (status IN ( 'pending', 'in_progress', 'completed', 'completed_with_discrepancies', 'failed' )) ); -- Indices CREATE INDEX idx_reconciliation_batches_tenant ON integrations.reconciliation_batches(tenant_id); CREATE INDEX idx_reconciliation_batches_provider ON integrations.reconciliation_batches(provider_id); CREATE INDEX idx_reconciliation_batches_date ON integrations.reconciliation_batches(batch_date DESC); CREATE INDEX idx_reconciliation_batches_status ON integrations.reconciliation_batches(status); -- RLS ALTER TABLE integrations.reconciliation_batches ENABLE ROW LEVEL SECURITY; CREATE POLICY reconciliation_batches_tenant_isolation ON integrations.reconciliation_batches FOR ALL USING (tenant_id = current_setting('app.tenant_id')::uuid); -- Comentarios COMMENT ON TABLE integrations.reconciliation_batches IS 'Lotes de conciliacion con proveedores'; ``` --- ## Funciones de Utilidad ### Procesar Webhook de Pago ```sql CREATE OR REPLACE FUNCTION integrations.process_payment_webhook( p_provider_type VARCHAR, p_event_type VARCHAR, p_external_id VARCHAR, p_status VARCHAR, p_gateway_response JSONB ) RETURNS UUID AS $$ DECLARE v_transaction_id UUID; v_old_status VARCHAR; BEGIN -- Buscar transaccion SELECT id, status INTO v_transaction_id, v_old_status FROM integrations.payment_transactions WHERE external_id = p_external_id; IF v_transaction_id IS NULL THEN RAISE EXCEPTION 'Transaction not found: %', p_external_id; END IF; -- Actualizar estado UPDATE integrations.payment_transactions SET status = p_status, gateway_response = gateway_response || p_gateway_response, processed_at = CASE WHEN p_status IN ('approved', 'rejected') THEN NOW() ELSE processed_at END, updated_at = NOW() WHERE id = v_transaction_id; -- TODO: Notificar cambio de estado via MGN-014 RETURN v_transaction_id; END; $$ LANGUAGE plpgsql; ``` ### Obtener Estadisticas de Proveedor ```sql CREATE OR REPLACE FUNCTION integrations.get_provider_stats( p_provider_id UUID, p_date_from DATE DEFAULT CURRENT_DATE - 30, p_date_to DATE DEFAULT CURRENT_DATE ) RETURNS TABLE ( total_transactions INT, total_amount DECIMAL, approved_count INT, approved_amount DECIMAL, rejected_count INT, avg_transaction_amount DECIMAL, approval_rate DECIMAL ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*)::INT AS total_transactions, COALESCE(SUM(amount), 0) AS total_amount, COUNT(*) FILTER (WHERE status = 'approved')::INT AS approved_count, COALESCE(SUM(amount) FILTER (WHERE status = 'approved'), 0) AS approved_amount, COUNT(*) FILTER (WHERE status = 'rejected')::INT AS rejected_count, COALESCE(AVG(amount) FILTER (WHERE status = 'approved'), 0) AS avg_transaction_amount, CASE WHEN COUNT(*) > 0 THEN (COUNT(*) FILTER (WHERE status = 'approved')::DECIMAL / COUNT(*) * 100) ELSE 0 END AS approval_rate FROM integrations.payment_transactions WHERE provider_id = p_provider_id AND created_at::date BETWEEN p_date_from AND p_date_to; END; $$ LANGUAGE plpgsql; ``` --- ## Vistas ### Vista: Transacciones Recientes ```sql CREATE VIEW integrations.vw_recent_transactions AS SELECT pt.id, pt.tenant_id, pp.provider_type, pp.name AS provider_name, pterm.name AS terminal_name, pt.external_id, pt.order_id, pt.amount, pt.currency, pt.payment_method, pt.card_brand, pt.card_last4, pt.status, pt.created_at, pt.processed_at FROM integrations.payment_transactions pt JOIN integrations.payment_providers pp ON pp.id = pt.provider_id LEFT JOIN integrations.payment_terminals pterm ON pterm.id = pt.terminal_id WHERE pt.created_at > CURRENT_TIMESTAMP - INTERVAL '7 days'; ``` ### Vista: Estado de Proveedores ```sql CREATE VIEW integrations.vw_provider_status AS SELECT pp.id, pp.tenant_id, pp.provider_type, pp.name, pp.is_active, pp.is_sandbox, pp.connection_status, pp.last_connected_at, pc.is_valid AS credentials_valid, pc.token_expires_at, (SELECT COUNT(*) FROM integrations.payment_terminals t WHERE t.provider_id = pp.id AND t.is_active) AS active_terminals, (SELECT COUNT(*) FROM integrations.payment_transactions t WHERE t.provider_id = pp.id AND t.created_at > CURRENT_DATE) AS transactions_today, (SELECT SUM(amount) FROM integrations.payment_transactions t WHERE t.provider_id = pp.id AND t.status = 'approved' AND t.created_at > CURRENT_DATE) AS amount_today FROM integrations.payment_providers pp LEFT JOIN integrations.payment_credentials pc ON pc.provider_id = pp.id; ``` --- ## Resumen de Tablas | Tabla | Columnas | Descripcion | |-------|----------|-------------| | payment_providers | 15 | Proveedores de pago por tenant | | payment_credentials | 14 | Credenciales encriptadas | | payment_terminals | 18 | Terminales fisicas | | payment_transactions | 27 | Transacciones de cobro | | refunds | 14 | Devoluciones | | webhook_logs | 14 | Logs de webhooks | | reconciliation_batches | 16 | Lotes de conciliacion | **Total: 7 tablas, 118 columnas** --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | System | Creacion inicial (MGN-016) |