erp-core/docs/04-modelado/database-design/DDL-SPEC-integrations.md

680 lines
21 KiB
Markdown

# 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) |