Actualiza documentación de integraciones de pagos a estructura estándar de 11-13 secciones siguiendo template de INT-001. Cambios por integración: INT-004-mercadopago.md (+458 líneas): - Actualizado simco_version a 4.0.1 - Agregado: Rate Limits con retry strategy - Agregado: Manejo de Errores completo (8 códigos) - Agregado: Fallbacks y modo degradado - Mejorado: Multi-tenant con SQL schema - Agregado: Webhooks IPN con validación de firma - Agregado: Testing con tarjetas de prueba MX - Agregado: Monitoreo con métricas y logs INT-005-clip.md (+485 líneas): - Actualizado simco_version a 4.0.1 - Agregado: Rate Limits (100 req/min) - Agregado: Manejo de Errores (7 códigos) - Agregado: Fallbacks con cola Redis - Agregado: Multi-tenant con tenant_clip_config - Agregado: Webhooks con HMAC validation - Agregado: Testing con tarjetas Clip MX - Agregado: Monitoreo y Referencias INT-006-codi-banxico.md (+694 líneas): - Actualizado simco_version a 4.0.1 - Agregado: Rate Limits STP/Banxico - Agregado: Manejo de Errores CoDi/STP - Agregado: Fallbacks (QR alternativo, manual) - Agregado: Multi-tenant con CLABEs por tenant - Agregado: Webhooks STP con firma RSA-SHA256 - Agregado: Testing con CLABEs sandbox - Agregado: Monitoreo y normatividad mexicana Total: +1,573 líneas de documentación técnica. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
823 lines
24 KiB
Markdown
823 lines
24 KiB
Markdown
---
|
|
id: INT-006
|
|
type: Integration
|
|
title: "Pagos CoDi/SPEI"
|
|
provider: "Banxico/STP"
|
|
status: Mock
|
|
integration_type: "Pagos QR instantáneos"
|
|
created_at: 2026-01-10
|
|
updated_at: 2026-01-17
|
|
simco_version: "4.0.1"
|
|
tags:
|
|
- pagos
|
|
- codi
|
|
- spei
|
|
- qr
|
|
- banxico
|
|
- transferencias
|
|
---
|
|
|
|
# INT-006: Pagos CoDi/SPEI
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **Codigo** | INT-006 |
|
|
| **Proveedor** | Banxico/STP |
|
|
| **Tipo** | Pagos QR instantáneos |
|
|
| **Estado** | Mock (en desarrollo) |
|
|
| **Multi-tenant** | Si |
|
|
| **Fecha planeada** | 2026-Q1 |
|
|
| **Owner** | Backend Team |
|
|
|
|
---
|
|
|
|
## 1. Descripcion
|
|
|
|
Integración con el sistema de Cobro Digital (CoDi) de Banxico y transferencias SPEI a través de STP (Sistema de Transferencias y Pagos). Permite a los changarros recibir pagos instantáneos mediante códigos QR, sin comisiones por transacción. CoDi opera sobre la infraestructura de SPEI, garantizando transferencias en tiempo real 24/7.
|
|
|
|
**Casos de uso principales:**
|
|
- Generación de códigos QR para cobro instantáneo
|
|
- Recepción de pagos SPEI sin comisión
|
|
- Notificación en tiempo real de pagos recibidos
|
|
- Conciliación automática de transacciones
|
|
- Generación de comprobantes electrónicos de pago
|
|
|
|
---
|
|
|
|
## 2. Credenciales Requeridas
|
|
|
|
### Variables de Entorno
|
|
|
|
| Variable | Descripcion | Tipo | Obligatorio |
|
|
|----------|-------------|------|-------------|
|
|
| STP_EMPRESA | Identificador de empresa en STP | string | SI |
|
|
| STP_CLAVE_PRIVADA | Llave privada para firmar mensajes | string | SI |
|
|
| STP_CERTIFICADO | Certificado digital .cer | string | SI |
|
|
| STP_CUENTA_CLABE | CLABE de la cuenta receptora | string | SI |
|
|
| STP_WEBHOOK_URL | URL para recibir notificaciones | string | SI |
|
|
| CODI_MERCHANT_ID | ID de comercio registrado en CoDi | string | SI |
|
|
| CODI_API_KEY | Llave de API de CoDi | string | SI |
|
|
|
|
### Ejemplo de .env
|
|
|
|
```env
|
|
# Pagos CoDi/SPEI
|
|
STP_EMPRESA=MICHANGARRITO
|
|
STP_CLAVE_PRIVADA=/path/to/private_key.pem
|
|
STP_CERTIFICADO=/path/to/certificate.cer
|
|
STP_CUENTA_CLABE=646180157000000001
|
|
STP_WEBHOOK_URL=https://api.michangarrito.mx/webhooks/stp
|
|
CODI_MERCHANT_ID=codi_mer_xxxxxxxx
|
|
CODI_API_KEY=codi_key_xxxxxxxx
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Arquitectura
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ MiChangarrito │
|
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
|
|
│ │ Frontend │───▶│ API Server │───▶│ CoDiService │ │
|
|
│ │ (POS/App) │ │ │ │ │ │
|
|
│ └──────────────┘ └──────────────┘ └────────┬─────────┘ │
|
|
└────────────────────────────────────────────────────┼────────────┘
|
|
│
|
|
┌─────────────────────────┼─────────────┐
|
|
│ ▼ │
|
|
│ ┌──────────────────────────────┐ │
|
|
│ │ STP Gateway │ │
|
|
│ └──────────────┬───────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌──────────────────────────────┐ │
|
|
│ │ Banxico SPEI / CoDi │ │
|
|
│ │ (Red Interbancaria MX) │ │
|
|
│ └──────────────────────────────┘ │
|
|
│ Sistema Financiero │
|
|
└───────────────────────────────────────┘
|
|
```
|
|
|
|
### Flujo de Pago CoDi
|
|
|
|
1. Comercio genera solicitud de cobro con monto
|
|
2. Sistema genera código QR con datos CoDi
|
|
3. Cliente escanea QR con app bancaria
|
|
4. Banco del cliente procesa transferencia SPEI
|
|
5. STP notifica la recepción del pago
|
|
6. Sistema actualiza estado de la venta en tiempo real
|
|
|
|
---
|
|
|
|
## 4. Endpoints
|
|
|
|
### Endpoints Consumidos (STP/CoDi API)
|
|
|
|
| Método | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| POST | `/v1/codi/cobros` | Generar solicitud de cobro CoDi |
|
|
| GET | `/v1/codi/cobros/{id}` | Consultar estado de cobro |
|
|
| POST | `/v1/spei/ordenPago` | Registrar orden de pago SPEI |
|
|
| GET | `/v1/spei/consultaOrden/{id}` | Consultar orden SPEI |
|
|
| GET | `/v1/cuentas/{clabe}/movimientos` | Consultar movimientos |
|
|
|
|
### Endpoints Expuestos (MiChangarrito)
|
|
|
|
| Método | Endpoint | Descripción |
|
|
|--------|----------|-------------|
|
|
| POST | `/api/v1/payments/codi/generate` | Generar QR de cobro |
|
|
| GET | `/api/v1/payments/codi/{id}/status` | Consultar estado |
|
|
| GET | `/api/v1/payments/codi/{id}/qr` | Obtener imagen QR |
|
|
| POST | `/api/v1/webhooks/stp` | Recibir notificaciones STP |
|
|
| GET | `/api/v1/payments/spei/movements` | Listar movimientos |
|
|
|
|
---
|
|
|
|
## 5. Rate Limits
|
|
|
|
### Limites de STP API
|
|
|
|
| Recurso | Limite | Periodo | Estrategia |
|
|
|---------|--------|---------|------------|
|
|
| Generacion de cobros CoDi | 100 | por minuto | Throttling |
|
|
| Consulta de estado | 300 | por minuto | Cache 10s |
|
|
| Consulta de movimientos | 60 | por minuto | Cache 30s |
|
|
| Ordenes SPEI | 50 | por minuto | Queue |
|
|
|
|
### Limites de Banxico/SPEI
|
|
|
|
| Tipo de Operacion | Limite | Observaciones |
|
|
|-------------------|--------|---------------|
|
|
| Monto maximo por transferencia | $8,000 MXN | Limite CoDi estandar |
|
|
| Monto maximo SPEI | Sin limite | Depende del banco |
|
|
| Transferencias por dia | Sin limite | Sujeto a monitoreo AML |
|
|
|
|
### Estrategia de Retry
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.retry.ts
|
|
export const codiRetryConfig = {
|
|
maxRetries: 3,
|
|
initialDelay: 1000, // 1 segundo
|
|
maxDelay: 30000, // 30 segundos
|
|
backoffMultiplier: 2,
|
|
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
|
|
// Errores especificos de STP que permiten retry
|
|
retryableSTPCodes: [
|
|
'STP_TIMEOUT',
|
|
'STP_SERVICE_UNAVAILABLE',
|
|
'STP_RATE_LIMITED'
|
|
]
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Manejo de Errores
|
|
|
|
### Codigos de Error STP/CoDi
|
|
|
|
| Codigo | Mensaje | Causa | Accion |
|
|
|--------|---------|-------|--------|
|
|
| `CODI_001` | Comercio no registrado | CLABE no asociada a CoDi | Verificar registro con Banxico |
|
|
| `CODI_002` | Monto excede limite | Mayor a $8,000 MXN | Usar SPEI directo |
|
|
| `CODI_003` | QR expirado | Timeout de 5 minutos | Generar nuevo QR |
|
|
| `CODI_004` | Cuenta invalida | CLABE con digito verificador incorrecto | Validar CLABE |
|
|
| `STP_001` | Firma invalida | Error en certificado o llave privada | Verificar credenciales |
|
|
| `STP_002` | Cuenta inexistente | CLABE destino no existe | Notificar al usuario |
|
|
| `STP_003` | Fondos insuficientes | Saldo insuficiente en cuenta origen | N/A (lado cliente) |
|
|
| `STP_004` | Servicio no disponible | Mantenimiento STP | Reintentar con backoff |
|
|
| `STP_005` | Operacion duplicada | Referencia ya procesada | Verificar estado existente |
|
|
| `SPEI_001` | Banco destino no disponible | Banco fuera de linea | Reintentar o notificar |
|
|
|
|
### Ejemplo de Manejo de Errores
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.error-handler.ts
|
|
import { Injectable, Logger } from '@nestjs/common';
|
|
import { CodiError, STPError } from './codi.types';
|
|
|
|
@Injectable()
|
|
export class CodiErrorHandler {
|
|
private readonly logger = new Logger(CodiErrorHandler.name);
|
|
|
|
async handleError(error: CodiError | STPError): Promise<PaymentErrorResponse> {
|
|
this.logger.error(`CoDi/STP Error: ${error.code}`, {
|
|
code: error.code,
|
|
message: error.message,
|
|
referencia: error.referencia,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
switch (error.code) {
|
|
case 'CODI_002':
|
|
return {
|
|
success: false,
|
|
error: 'AMOUNT_EXCEEDED',
|
|
message: 'El monto excede el limite de CoDi ($8,000 MXN). Use transferencia SPEI.',
|
|
suggestion: 'spei_transfer',
|
|
retryable: false
|
|
};
|
|
|
|
case 'CODI_003':
|
|
return {
|
|
success: false,
|
|
error: 'QR_EXPIRED',
|
|
message: 'El codigo QR ha expirado. Genere uno nuevo.',
|
|
suggestion: 'regenerate_qr',
|
|
retryable: true
|
|
};
|
|
|
|
case 'STP_004':
|
|
return {
|
|
success: false,
|
|
error: 'SERVICE_UNAVAILABLE',
|
|
message: 'El servicio de pagos no esta disponible temporalmente.',
|
|
suggestion: 'retry_later',
|
|
retryable: true,
|
|
retryAfter: 30 // segundos
|
|
};
|
|
|
|
default:
|
|
return {
|
|
success: false,
|
|
error: 'PAYMENT_ERROR',
|
|
message: 'Error procesando el pago. Intente nuevamente.',
|
|
retryable: true
|
|
};
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Fallbacks
|
|
|
|
### Estrategia de Fallback
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ Pago CoDi │
|
|
│ (Primario) │
|
|
└────────┬────────┘
|
|
│ Falla?
|
|
▼
|
|
┌─────────────────┐
|
|
│ QR Alternativo │
|
|
│ (Transferencia │
|
|
│ SPEI manual) │
|
|
└────────┬────────┘
|
|
│ Falla?
|
|
▼
|
|
┌─────────────────┐
|
|
│ Notificacion │
|
|
│ Manual + Datos │
|
|
│ de Cuenta │
|
|
└─────────────────┘
|
|
```
|
|
|
|
### Configuracion de Fallbacks
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.fallback.ts
|
|
export const codiFallbackConfig = {
|
|
// Fallback 1: QR con datos de transferencia SPEI
|
|
speiQR: {
|
|
enabled: true,
|
|
includeData: ['clabe', 'beneficiario', 'concepto', 'monto'],
|
|
qrFormat: 'EMVCo' // Compatible con apps bancarias
|
|
},
|
|
|
|
// Fallback 2: Datos para transferencia manual
|
|
manualTransfer: {
|
|
enabled: true,
|
|
showBankData: true,
|
|
copyToClipboard: true
|
|
},
|
|
|
|
// Notificaciones en modo degradado
|
|
notifications: {
|
|
sms: true, // Enviar datos por SMS
|
|
email: true // Enviar datos por email
|
|
}
|
|
};
|
|
```
|
|
|
|
### Modo Degradado
|
|
|
|
Cuando STP/CoDi no esta disponible:
|
|
|
|
1. **Deteccion automatica**: Health check cada 30 segundos
|
|
2. **Activacion**: Despues de 3 fallos consecutivos
|
|
3. **Comportamiento**:
|
|
- Mostrar QR con datos CLABE para transferencia manual
|
|
- Habilitar conciliacion manual en backoffice
|
|
- Alertar al administrador del tenant
|
|
4. **Recuperacion**: Automatica cuando servicio responde OK
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.health.ts
|
|
@Injectable()
|
|
export class CodiHealthService {
|
|
private degradedMode = false;
|
|
private consecutiveFailures = 0;
|
|
|
|
async checkHealth(): Promise<boolean> {
|
|
try {
|
|
const response = await this.stpClient.ping();
|
|
if (response.ok) {
|
|
this.consecutiveFailures = 0;
|
|
this.degradedMode = false;
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
this.consecutiveFailures++;
|
|
if (this.consecutiveFailures >= 3) {
|
|
this.degradedMode = true;
|
|
this.notifyAdmin('CoDi en modo degradado');
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isDegraded(): boolean {
|
|
return this.degradedMode;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Multi-tenant
|
|
|
|
### Modelo de Credenciales por Tenant
|
|
|
|
Cada tenant de MiChangarrito tiene su propia cuenta CLABE registrada en CoDi/STP, permitiendo:
|
|
- Recepcion directa de pagos a cuenta del comercio
|
|
- Conciliacion independiente por tenant
|
|
- Reportes fiscales separados
|
|
|
|
### Estructura SQL
|
|
|
|
```sql
|
|
-- Tabla de configuracion CoDi por tenant
|
|
CREATE TABLE tenant_codi_config (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
|
|
|
-- Credenciales STP
|
|
stp_empresa VARCHAR(50) NOT NULL,
|
|
stp_cuenta_clabe VARCHAR(18) NOT NULL,
|
|
stp_certificado_path VARCHAR(255),
|
|
stp_private_key_encrypted TEXT,
|
|
|
|
-- Credenciales CoDi
|
|
codi_merchant_id VARCHAR(50),
|
|
codi_api_key_encrypted TEXT,
|
|
|
|
-- Configuracion
|
|
webhook_secret_encrypted TEXT,
|
|
is_active BOOLEAN DEFAULT true,
|
|
sandbox_mode BOOLEAN DEFAULT false,
|
|
|
|
-- Limites personalizados
|
|
daily_limit DECIMAL(12,2) DEFAULT 50000.00,
|
|
transaction_limit DECIMAL(12,2) DEFAULT 8000.00,
|
|
|
|
-- Auditoria
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW(),
|
|
|
|
CONSTRAINT unique_tenant_codi UNIQUE(tenant_id),
|
|
CONSTRAINT valid_clabe CHECK (LENGTH(stp_cuenta_clabe) = 18)
|
|
);
|
|
|
|
-- Indice para busqueda por CLABE (notificaciones entrantes)
|
|
CREATE INDEX idx_tenant_codi_clabe ON tenant_codi_config(stp_cuenta_clabe);
|
|
```
|
|
|
|
### Ejemplo de Contexto Multi-tenant
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.context.ts
|
|
import { Injectable, Scope, Inject } from '@nestjs/common';
|
|
import { REQUEST } from '@nestjs/core';
|
|
import { Request } from 'express';
|
|
|
|
@Injectable({ scope: Scope.REQUEST })
|
|
export class CodiContextService {
|
|
constructor(
|
|
@Inject(REQUEST) private request: Request,
|
|
private readonly configRepo: TenantCodiConfigRepository
|
|
) {}
|
|
|
|
async getCodiConfig(): Promise<TenantCodiConfig> {
|
|
const tenantId = this.request['tenantId'];
|
|
|
|
const config = await this.configRepo.findByTenantId(tenantId);
|
|
if (!config || !config.isActive) {
|
|
throw new PaymentConfigurationError(
|
|
'CoDi no configurado para este comercio'
|
|
);
|
|
}
|
|
|
|
return {
|
|
...config,
|
|
privateKey: await this.decryptKey(config.stpPrivateKeyEncrypted),
|
|
apiKey: await this.decryptKey(config.codiApiKeyEncrypted)
|
|
};
|
|
}
|
|
|
|
async generateCodiQR(amount: number, concept: string): Promise<CodiQR> {
|
|
const config = await this.getCodiConfig();
|
|
|
|
return this.codiService.generateQR({
|
|
clabe: config.stpCuentaClabe,
|
|
monto: amount,
|
|
concepto: concept,
|
|
merchantId: config.codiMerchantId,
|
|
referencia: this.generateReference()
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Webhooks
|
|
|
|
### Endpoints Registrados para Notificaciones STP
|
|
|
|
| Evento | Endpoint | Metodo |
|
|
|--------|----------|--------|
|
|
| Pago recibido | `/api/v1/webhooks/stp/payment-received` | POST |
|
|
| Pago rechazado | `/api/v1/webhooks/stp/payment-rejected` | POST |
|
|
| Devolucion | `/api/v1/webhooks/stp/refund` | POST |
|
|
| Estado de orden | `/api/v1/webhooks/stp/order-status` | POST |
|
|
|
|
### Validacion de Firma Digital
|
|
|
|
STP firma todas las notificaciones con su certificado. Es **obligatorio** validar la firma antes de procesar.
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.webhook-validator.ts
|
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
import * as crypto from 'crypto';
|
|
|
|
@Injectable()
|
|
export class CodiWebhookValidator {
|
|
private readonly stpPublicKey: string;
|
|
|
|
constructor(private configService: ConfigService) {
|
|
this.stpPublicKey = this.configService.get('STP_PUBLIC_KEY');
|
|
}
|
|
|
|
validateSignature(payload: string, signature: string): boolean {
|
|
const verifier = crypto.createVerify('RSA-SHA256');
|
|
verifier.update(payload);
|
|
|
|
const isValid = verifier.verify(
|
|
this.stpPublicKey,
|
|
signature,
|
|
'base64'
|
|
);
|
|
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Firma STP invalida');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async processWebhook(
|
|
headers: Record<string, string>,
|
|
body: STPWebhookPayload
|
|
): Promise<void> {
|
|
// 1. Validar firma
|
|
const signature = headers['x-stp-signature'];
|
|
this.validateSignature(JSON.stringify(body), signature);
|
|
|
|
// 2. Validar timestamp (prevenir replay attacks)
|
|
const timestamp = parseInt(headers['x-stp-timestamp']);
|
|
const now = Date.now();
|
|
if (Math.abs(now - timestamp) > 300000) { // 5 minutos
|
|
throw new UnauthorizedException('Timestamp fuera de rango');
|
|
}
|
|
|
|
// 3. Verificar idempotencia
|
|
const eventId = headers['x-stp-event-id'];
|
|
if (await this.isProcessed(eventId)) {
|
|
return; // Ya procesado, ignorar
|
|
}
|
|
|
|
// 4. Procesar segun tipo de evento
|
|
await this.handleEvent(body);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Configuracion de Webhooks en STP
|
|
|
|
```yaml
|
|
# Configuracion a registrar en panel STP
|
|
webhooks:
|
|
production:
|
|
url: https://api.michangarrito.mx/api/v1/webhooks/stp
|
|
events:
|
|
- payment.received
|
|
- payment.rejected
|
|
- refund.completed
|
|
authentication:
|
|
type: signature
|
|
algorithm: RSA-SHA256
|
|
retry:
|
|
max_attempts: 5
|
|
interval: exponential
|
|
|
|
sandbox:
|
|
url: https://staging-api.michangarrito.mx/api/v1/webhooks/stp
|
|
events:
|
|
- "*"
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Testing
|
|
|
|
### Modo Sandbox de STP
|
|
|
|
STP proporciona un ambiente de pruebas completo:
|
|
|
|
| Ambiente | URL Base | Proposito |
|
|
|----------|----------|-----------|
|
|
| Sandbox | `https://sandbox.stpmex.com/v1` | Desarrollo y pruebas |
|
|
| Produccion | `https://api.stpmex.com/v1` | Operacion real |
|
|
|
|
### Datos de Prueba
|
|
|
|
```typescript
|
|
// src/integrations/codi/__tests__/codi.test-data.ts
|
|
export const testData = {
|
|
// CLABEs de prueba (sandbox STP)
|
|
clabes: {
|
|
valid: '646180157000000001',
|
|
invalid: '000000000000000000',
|
|
insufficientFunds: '646180157000000002'
|
|
},
|
|
|
|
// Montos de prueba
|
|
amounts: {
|
|
success: 100.00,
|
|
exceedsLimit: 10000.00,
|
|
timeout: 999.99 // Trigger timeout en sandbox
|
|
},
|
|
|
|
// Referencias
|
|
references: {
|
|
success: 'TEST-SUCCESS-001',
|
|
duplicate: 'TEST-DUPLICATE-001',
|
|
rejected: 'TEST-REJECTED-001'
|
|
}
|
|
};
|
|
```
|
|
|
|
### Comandos de Verificacion
|
|
|
|
```bash
|
|
# Verificar conectividad con STP sandbox
|
|
curl -X GET https://sandbox.stpmex.com/v1/health \
|
|
-H "Authorization: Bearer $STP_API_KEY"
|
|
|
|
# Generar QR de prueba
|
|
curl -X POST https://sandbox.stpmex.com/v1/codi/cobros \
|
|
-H "Authorization: Bearer $STP_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"monto": 100.00,
|
|
"concepto": "Prueba MiChangarrito",
|
|
"clabe": "646180157000000001",
|
|
"referencia": "TEST-001"
|
|
}'
|
|
|
|
# Consultar estado de cobro
|
|
curl -X GET https://sandbox.stpmex.com/v1/codi/cobros/TEST-001 \
|
|
-H "Authorization: Bearer $STP_API_KEY"
|
|
|
|
# Simular webhook de pago recibido (solo sandbox)
|
|
curl -X POST https://sandbox.stpmex.com/v1/test/simulate-payment \
|
|
-H "Authorization: Bearer $STP_API_KEY" \
|
|
-d '{"referencia": "TEST-001", "status": "completed"}'
|
|
```
|
|
|
|
### Tests Automatizados
|
|
|
|
```typescript
|
|
// src/integrations/codi/__tests__/codi.service.spec.ts
|
|
describe('CodiService', () => {
|
|
describe('generateQR', () => {
|
|
it('should generate valid CoDi QR', async () => {
|
|
const result = await codiService.generateQR({
|
|
monto: 100.00,
|
|
concepto: 'Test payment',
|
|
referencia: 'TEST-001'
|
|
});
|
|
|
|
expect(result.qrCode).toBeDefined();
|
|
expect(result.expiresAt).toBeInstanceOf(Date);
|
|
expect(result.referencia).toBe('TEST-001');
|
|
});
|
|
|
|
it('should reject amount over CoDi limit', async () => {
|
|
await expect(codiService.generateQR({
|
|
monto: 10000.00, // Excede $8,000
|
|
concepto: 'Test',
|
|
referencia: 'TEST-002'
|
|
})).rejects.toThrow('CODI_002');
|
|
});
|
|
});
|
|
|
|
describe('webhook validation', () => {
|
|
it('should validate STP signature', async () => {
|
|
const validPayload = signWithTestKey(testPayload);
|
|
const result = await webhookValidator.validateSignature(
|
|
testPayload,
|
|
validPayload.signature
|
|
);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it('should reject invalid signature', async () => {
|
|
await expect(webhookValidator.validateSignature(
|
|
testPayload,
|
|
'invalid-signature'
|
|
)).rejects.toThrow(UnauthorizedException);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Monitoreo
|
|
|
|
### Metricas Especificas
|
|
|
|
| Metrica | Descripcion | Alerta |
|
|
|---------|-------------|--------|
|
|
| `codi.qr.generated` | QRs generados por minuto | > 100/min warning |
|
|
| `codi.payment.confirmed` | Pagos confirmados | - |
|
|
| `codi.payment.confirmation_time` | Tiempo entre QR y pago | > 120s warning |
|
|
| `codi.payment.success_rate` | Tasa de exito de pagos | < 95% critical |
|
|
| `codi.webhook.received` | Webhooks recibidos | - |
|
|
| `codi.webhook.validation_failed` | Firmas invalidas | > 0 critical |
|
|
| `stp.api.latency` | Latencia API STP | > 2s warning |
|
|
| `stp.api.errors` | Errores de API | > 5/min critical |
|
|
|
|
### Logs Estructurados
|
|
|
|
```typescript
|
|
// src/integrations/codi/codi.logger.ts
|
|
import { Logger } from '@nestjs/common';
|
|
|
|
export class CodiLogger {
|
|
private readonly logger = new Logger('CoDi');
|
|
|
|
logQRGenerated(data: {
|
|
tenantId: string;
|
|
referencia: string;
|
|
monto: number;
|
|
clabe: string;
|
|
}) {
|
|
this.logger.log({
|
|
event: 'codi.qr.generated',
|
|
tenantId: data.tenantId,
|
|
referencia: data.referencia,
|
|
monto: data.monto,
|
|
clabe: this.maskClabe(data.clabe),
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
logPaymentReceived(data: {
|
|
tenantId: string;
|
|
referencia: string;
|
|
monto: number;
|
|
ordenante: string;
|
|
confirmationTime: number;
|
|
}) {
|
|
this.logger.log({
|
|
event: 'codi.payment.received',
|
|
tenantId: data.tenantId,
|
|
referencia: data.referencia,
|
|
monto: data.monto,
|
|
ordenante: this.maskName(data.ordenante),
|
|
confirmationTimeMs: data.confirmationTime,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
logWebhookError(data: {
|
|
eventId: string;
|
|
error: string;
|
|
signature: string;
|
|
}) {
|
|
this.logger.error({
|
|
event: 'codi.webhook.validation_failed',
|
|
eventId: data.eventId,
|
|
error: data.error,
|
|
signaturePrefix: data.signature.substring(0, 20),
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
private maskClabe(clabe: string): string {
|
|
return clabe.substring(0, 6) + '******' + clabe.substring(12);
|
|
}
|
|
|
|
private maskName(name: string): string {
|
|
return name.substring(0, 2) + '***';
|
|
}
|
|
}
|
|
```
|
|
|
|
### Dashboard Recomendado
|
|
|
|
```yaml
|
|
# Grafana dashboard config
|
|
panels:
|
|
- title: "Pagos CoDi - Tiempo Real"
|
|
type: stat
|
|
query: "sum(rate(codi_payment_confirmed_total[5m]))"
|
|
|
|
- title: "Tiempo de Confirmacion"
|
|
type: gauge
|
|
query: "histogram_quantile(0.95, codi_payment_confirmation_seconds)"
|
|
thresholds:
|
|
- value: 30
|
|
color: green
|
|
- value: 60
|
|
color: yellow
|
|
- value: 120
|
|
color: red
|
|
|
|
- title: "Tasa de Exito"
|
|
type: gauge
|
|
query: "codi_payment_success_rate"
|
|
thresholds:
|
|
- value: 99
|
|
color: green
|
|
- value: 95
|
|
color: yellow
|
|
- value: 0
|
|
color: red
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Referencias
|
|
|
|
### Documentacion Oficial
|
|
|
|
| Recurso | URL | Descripcion |
|
|
|---------|-----|-------------|
|
|
| Banxico CoDi | https://www.banxico.org.mx/sistemas-de-pago/codi-702.html | Especificacion oficial CoDi |
|
|
| STP API Docs | https://stpmex.com/documentacion | Documentacion API STP |
|
|
| SPEI Reglas | https://www.banxico.org.mx/sistemas-de-pago/spei-702.html | Reglas operativas SPEI |
|
|
| EMVCo QR | https://www.emvco.com/emv-technologies/qr-code/ | Estandar QR para pagos |
|
|
|
|
### Modulos Relacionados (MiChangarrito)
|
|
|
|
| Modulo | Descripcion | Relacion |
|
|
|--------|-------------|----------|
|
|
| `MOD-003` | Pagos | Modulo padre de integraciones de pago |
|
|
| `INT-001` | Stripe | Alternativa para pagos con tarjeta |
|
|
| `INT-007` | Facturacion SAT | Generacion de CFDI post-pago |
|
|
| `SEC-002` | Encriptacion | Almacenamiento seguro de credenciales |
|
|
|
|
### Normativas Aplicables
|
|
|
|
- **Ley Fintech**: Regulacion de instituciones de tecnologia financiera
|
|
- **Circular 4/2019 Banxico**: Reglas de operacion CoDi
|
|
- **PCI DSS**: Aunque CoDi no maneja tarjetas, aplica para datos sensibles
|
|
- **LFPDPPP**: Proteccion de datos personales de ordenantes
|
|
|
|
---
|
|
|
|
## 13. Notas de Implementacion
|
|
|
|
- CoDi no cobra comisiones por transaccion (beneficio vs tarjetas)
|
|
- Las transferencias SPEI son irreversibles una vez confirmadas
|
|
- Implementar timeout de 5 minutos para codigos QR
|
|
- Validar firma digital en todas las notificaciones de STP
|
|
- El registro como participante CoDi requiere tramite con Banxico
|
|
- Generar referencia unica por transaccion para conciliacion
|
|
- Los QR deben cumplir con el estandar EMVCo para CoDi
|
|
- Horario de operacion SPEI: 24/7, pero cortes contables a las 17:30
|
|
- Considerar implementar cache de CLABEs validadas
|
|
- Manejar reintentos en caso de falla de comunicacion con STP
|