--- 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 { 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 { 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 { 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 { 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, body: STPWebhookPayload ): Promise { // 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