[MCH-DOC-VAL] docs: Mejorar integraciones INT-004, INT-005, INT-006 con SIMCO 4.0.1

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>
This commit is contained in:
rckrdmrd 2026-01-17 04:46:57 -06:00
parent c659f1c623
commit 15852f2d6a
3 changed files with 1573 additions and 64 deletions

View File

@ -6,8 +6,8 @@ provider: "MercadoPago"
status: Pendiente
integration_type: "payments"
created_at: 2026-01-04
updated_at: 2026-01-10
simco_version: "3.8.0"
updated_at: 2026-01-17
simco_version: "4.0.1"
tags:
- mercadopago
- payments
@ -27,7 +27,7 @@ tags:
| **Estado** | Pendiente |
| **Multi-tenant** | Si (por tenant) |
| **Fecha integracion** | - |
| **Ultimo update** | 2026-01-10 |
| **Ultimo update** | 2026-01-17 |
| **Owner** | Backend Team |
---
@ -76,11 +76,13 @@ MERCADOPAGO_WEBHOOK_SECRET=xxxxxxxx
### Operaciones Planificadas
| Operacion | SDK Method | Descripcion |
|-----------|------------|-------------|
| Crear Preferencia | `preference.create()` | Link de pago |
| Crear QR | `qr.create()` | Cobro por QR |
| Consultar Pago | `payment.get()` | Estado del pago |
| Operacion | SDK Method | Endpoint | Descripcion |
|-----------|------------|----------|-------------|
| Crear Preferencia | `preference.create()` | POST `/checkout/preferences` | Link de pago |
| Crear QR | `qr.create()` | POST `/instore/qr/seller` | Cobro por QR |
| Consultar Pago | `payment.get()` | GET `/v1/payments/{id}` | Estado del pago |
| Reembolso | `payment.refund()` | POST `/v1/payments/{id}/refunds` | Devolucion |
| Buscar Pagos | `payment.search()` | GET `/v1/payments/search` | Historial de pagos |
### SDK Planificado
@ -100,11 +102,15 @@ const result = await preference.create({
title: 'Venta en tienda',
unit_price: 100,
quantity: 1,
currency_id: 'MXN',
}],
back_urls: {
success: 'https://michangarrito.com/pago/exitoso',
failure: 'https://michangarrito.com/pago/fallido',
pending: 'https://michangarrito.com/pago/pendiente',
},
auto_return: 'approved',
external_reference: 'sale_12345',
},
});
```
@ -115,23 +121,138 @@ const result = await preference.create({
| Limite | Valor | Periodo | Accion si excede |
|--------|-------|---------|------------------|
| API calls | 10,000 | por minuto | 429 |
| API calls (general) | 10,000 | por minuto | 429 Too Many Requests |
| Crear preferencias | 1,000 | por minuto | 429 |
| Consultar pagos | 5,000 | por minuto | 429 |
| Webhooks entrantes | Sin limite | - | N/A |
### Estrategia de Retry
```typescript
const delays = [1000, 2000, 4000, 8000];
async function callWithRetry<T>(fn: () => Promise<T>): Promise<T> {
for (let attempt = 0; attempt < delays.length; attempt++) {
try {
return await fn();
} catch (error) {
if (error.status === 429) {
await sleep(delays[attempt]);
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
// Uso
const payment = await callWithRetry(() => paymentService.get(paymentId));
```
---
## 5. Manejo de Errores
### Codigos de Error (Planeados)
### Codigos de Error
| Codigo | Descripcion | Accion Recomendada |
|--------|-------------|-------------------|
| 400 | Parametros invalidos | Validar input |
| 401 | Token invalido | Verificar credenciales |
| 404 | Recurso no encontrado | Verificar ID |
| Codigo | Descripcion | Accion Recomendada | Retry |
|--------|-------------|-------------------|-------|
| 400 | Parametros invalidos | Validar payload antes de enviar | NO |
| 401 | Token invalido o expirado | Regenerar access token | NO |
| 402 | Pago rechazado | Informar al usuario, sugerir otro metodo | NO |
| 403 | Cuenta no autorizada | Verificar permisos de la aplicacion | NO |
| 404 | Recurso no encontrado | Verificar ID de pago/preferencia | NO |
| 429 | Rate limit excedido | Backoff exponencial | SI |
| 500 | Error interno MercadoPago | Retry con backoff | SI |
| 503 | Servicio no disponible | Retry con backoff, activar fallback | SI |
### Ejemplo de Manejo
```typescript
import { Logger } from '@nestjs/common';
try {
const payment = await mercadopagoService.createPayment(paymentData);
return { success: true, paymentId: payment.id };
} catch (error) {
const logger = new Logger('MercadoPago');
logger.error('MercadoPago error', {
service: 'mercadopago',
operation: 'createPayment',
status: error.status,
code: error.cause?.code,
message: error.message,
tenantId: context.tenantId,
externalReference: paymentData.external_reference,
});
// Clasificar error
if (error.status === 402) {
return { success: false, error: 'payment_rejected', userMessage: 'El pago fue rechazado' };
}
if (error.status === 429 || error.status >= 500) {
// Encolar para reintento
await this.queue.add('mercadopago-retry', { paymentData, tenantId: context.tenantId });
return { success: false, error: 'queued', userMessage: 'Procesando pago, te notificaremos' };
}
throw error;
}
```
---
## 6. Multi-tenant
## 6. Fallbacks
### Estrategia de Fallback
| Escenario | Fallback | Tiempo maximo |
|-----------|----------|---------------|
| API caida | Cola Redis para reintentar | 24 horas |
| Rate limited | Throttle + prioridad | 1 hora |
| Token expirado | Alerta admin + regenerar | Inmediato |
| Pago rechazado | Sugerir efectivo o transferencia | N/A |
### Modo Degradado
Si MercadoPago no esta disponible:
```typescript
async processPayment(saleId: string, amount: number, method: string) {
if (await this.mercadopagoHealthCheck()) {
return this.mercadopagoService.createPayment({ saleId, amount });
}
// Modo degradado: registrar pago pendiente
this.logger.warn('MercadoPago unavailable, entering degraded mode', {
service: 'mercadopago',
saleId,
});
// Opciones alternativas
return {
status: 'degraded',
alternatives: [
{ method: 'cash', message: 'Aceptar pago en efectivo' },
{ method: 'bank_transfer', message: 'Solicitar transferencia bancaria' },
],
pendingQueue: await this.queue.add('payment-pending', { saleId, amount }),
};
}
```
### Recuperacion Automatica
- Worker de Redis revisa cada 5 minutos si API esta disponible
- Reintentos automaticos de pagos encolados
- Notificacion al tenant cuando pago se procesa
---
## 7. Multi-tenant
### Modelo de Credenciales
@ -139,35 +260,309 @@ const result = await preference.create({
Los fondos van directo a la cuenta del dueno de la tienda.
### Almacenamiento Planificado
### Almacenamiento
```sql
CREATE TABLE sales.tenant_mercadopago_config (
id UUID PRIMARY KEY,
tenant_id UUID REFERENCES auth.tenants(id),
access_token TEXT NOT NULL, -- Encriptado
public_key VARCHAR(100),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES auth.tenants(id) NOT NULL,
access_token TEXT NOT NULL, -- Encriptado con AES-256
public_key VARCHAR(100) NOT NULL,
collector_id VARCHAR(50),
user_id VARCHAR(50),
is_sandbox BOOLEAN DEFAULT false,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(tenant_id)
);
-- Indice para busquedas
CREATE INDEX idx_mercadopago_config_tenant ON sales.tenant_mercadopago_config(tenant_id);
```
### Contexto en Llamadas
```typescript
async createPaymentForTenant(
tenantId: UUID,
saleId: string,
amount: number,
description: string
): Promise<PaymentResult> {
const config = await this.getTenantMercadopagoConfig(tenantId);
if (!config || !config.is_active) {
throw new MercadoPagoNotConfiguredError(tenantId);
}
// Crear cliente con credenciales del tenant
const client = new MercadoPagoConfig({
accessToken: this.decrypt(config.access_token),
});
const preference = new Preference(client);
return preference.create({
body: {
items: [{
title: description,
unit_price: amount,
quantity: 1,
currency_id: 'MXN',
}],
external_reference: saleId,
notification_url: `https://api.michangarrito.com/webhooks/mercadopago/${tenantId}`,
},
});
}
```
---
## 7. Webhooks (IPN)
## 8. Webhooks (IPN)
### Endpoints Planificados
### Endpoints Registrados
| Evento | Endpoint Local | Descripcion |
|--------|----------------|-------------|
| `payment` | `/webhooks/mercadopago` | Pago recibido |
| `merchant_order` | `/webhooks/mercadopago` | Orden actualizada |
| `payment` | `/webhooks/mercadopago/:tenantId` | Pago recibido/actualizado |
| `merchant_order` | `/webhooks/mercadopago/:tenantId` | Orden actualizada |
| `point_integration_ipn` | `/webhooks/mercadopago/:tenantId` | Terminal Point |
### Validacion de Firma IPN
```typescript
import * as crypto from 'crypto';
function verifyMercadoPagoSignature(
xSignature: string,
xRequestId: string,
dataId: string,
webhookSecret: string
): boolean {
// Extraer ts y hash del header x-signature
// Formato: ts=xxx,v1=hash
const parts = xSignature.split(',');
const ts = parts.find(p => p.startsWith('ts='))?.split('=')[1];
const hash = parts.find(p => p.startsWith('v1='))?.split('=')[1];
if (!ts || !hash) {
return false;
}
// Construir el manifest para validar
const manifest = `id:${dataId};request-id:${xRequestId};ts:${ts};`;
// Calcular HMAC
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(manifest)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expected));
}
// Uso en controller
@Post('webhooks/mercadopago/:tenantId')
async handleWebhook(
@Param('tenantId') tenantId: string,
@Headers('x-signature') signature: string,
@Headers('x-request-id') requestId: string,
@Query('data.id') dataId: string,
@Body() body: MercadoPagoWebhookPayload,
) {
const config = await this.getTenantConfig(tenantId);
if (!verifyMercadoPagoSignature(signature, requestId, dataId, config.webhook_secret)) {
throw new UnauthorizedException('Invalid webhook signature');
}
// Procesar el evento
await this.processWebhookEvent(tenantId, body);
return { received: true };
}
```
### Configuracion en MercadoPago
1. MercadoPago Dashboard → Tu aplicacion → Webhooks
2. URL de notificacion: `https://api.michangarrito.com/webhooks/mercadopago/{tenant_id}`
3. Eventos a suscribir: `payment`, `merchant_order`
4. Guardar y probar
---
## 8. Estado de Implementacion
## 9. Testing
### Modo Sandbox/Test
| Ambiente | Credenciales | Datos |
|----------|--------------|-------|
| Sandbox | Test Access Token | Tarjetas de prueba MercadoPago |
| Production | Production Access Token | Tarjetas reales |
### Tarjetas de Prueba (Mexico)
| Numero | CVV | Vencimiento | Resultado |
|--------|-----|-------------|-----------|
| 5474 9254 3267 0366 | 123 | 11/25 | Aprobado |
| 4509 9535 6623 3704 | 123 | 11/25 | Rechazado por fondos |
| 3711 803032 57522 | 1234 | 11/25 | Pendiente |
### Comandos de Test
```bash
# Test de creacion de preferencia
curl -X POST https://api.mercadopago.com/checkout/preferences \
-H "Authorization: Bearer ${MERCADOPAGO_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"items": [{
"title": "Test Product",
"quantity": 1,
"unit_price": 100,
"currency_id": "MXN"
}],
"external_reference": "test_001"
}'
# Consultar estado de pago
curl -X GET "https://api.mercadopago.com/v1/payments/${PAYMENT_ID}" \
-H "Authorization: Bearer ${MERCADOPAGO_ACCESS_TOKEN}"
# Test de webhook local
curl -X POST http://localhost:3143/webhooks/mercadopago/test-tenant \
-H "Content-Type: application/json" \
-H "x-signature: ts=1234567890,v1=test" \
-H "x-request-id: test-request-id" \
-d '{"action":"payment.created","data":{"id":"12345678"}}'
```
### Tests Unitarios
```typescript
describe('MercadoPagoService', () => {
it('should create preference successfully', async () => {
const mockPreference = { id: 'pref_123', init_point: 'https://...' };
jest.spyOn(preferenceClient, 'create').mockResolvedValue(mockPreference);
const result = await service.createPaymentLink({
saleId: 'sale_001',
amount: 100,
description: 'Test sale',
});
expect(result.id).toBe('pref_123');
expect(result.init_point).toBeDefined();
});
it('should handle rate limit with retry', async () => {
jest.spyOn(paymentClient, 'get')
.mockRejectedValueOnce({ status: 429 })
.mockResolvedValueOnce({ id: 'pay_123', status: 'approved' });
const result = await service.getPaymentWithRetry('pay_123');
expect(result.status).toBe('approved');
});
});
```
---
## 10. Monitoreo
### Metricas a Monitorear
| Metrica | Descripcion | Alerta |
|---------|-------------|--------|
| Latencia | Tiempo de respuesta API | > 5s |
| Error Rate | % de requests fallidos | > 5% |
| Payments Created | Pagos creados por hora | < 1 (posible caida) |
| Payment Success Rate | % de pagos aprobados | < 70% |
| Webhook Delay | Tiempo entre evento y procesamiento | > 30s |
| Queue Depth | Pagos pendientes en cola | > 100 |
### Logs Estructurados
```typescript
// Pago creado exitosamente
this.logger.info('MercadoPago payment created', {
service: 'mercadopago',
operation: 'createPayment',
tenantId: context.tenantId,
preferenceId: result.id,
externalReference: saleId,
amount: amount,
currency: 'MXN',
duration: durationMs,
});
// Webhook recibido
this.logger.info('MercadoPago webhook received', {
service: 'mercadopago',
operation: 'webhook',
tenantId: tenantId,
action: body.action,
paymentId: body.data?.id,
type: body.type,
});
// Error en pago
this.logger.error('MercadoPago payment failed', {
service: 'mercadopago',
operation: 'createPayment',
tenantId: context.tenantId,
errorCode: error.cause?.code,
errorMessage: error.message,
saleId: saleId,
});
```
### Dashboard Sugerido
```yaml
# Grafana dashboard panels
panels:
- title: "Pagos por Estado"
type: pie_chart
query: "mercadopago_payments_total BY status"
- title: "Latencia API"
type: time_series
query: "histogram_quantile(0.95, mercadopago_api_duration_seconds)"
- title: "Tasa de Errores"
type: stat
query: "rate(mercadopago_errors_total[5m]) / rate(mercadopago_requests_total[5m])"
```
---
## 11. Referencias
### Documentacion Oficial
- [MercadoPago Developers Mexico](https://www.mercadopago.com.mx/developers/es)
- [API Reference](https://www.mercadopago.com.mx/developers/es/reference)
- [SDK Node.js](https://github.com/mercadopago/sdk-nodejs)
- [Webhooks/IPN](https://www.mercadopago.com.mx/developers/es/docs/your-integrations/notifications/webhooks)
- [Checkout Pro](https://www.mercadopago.com.mx/developers/es/docs/checkout-pro/landing)
- [Tarjetas de Prueba](https://www.mercadopago.com.mx/developers/es/docs/your-integrations/test/cards)
### Modulos Relacionados
- [Sales Module](../../apps/backend/src/modules/sales/)
- [Payments Service](../../apps/backend/src/modules/payments/)
- [Arquitectura Multi-Tenant](../90-transversal/ARQUITECTURA-MULTI-TENANT-INTEGRACIONES.md)
### Soporte
- MercadoPago Business Help: https://www.mercadopago.com.mx/ayuda
- Developer Support: https://www.mercadopago.com.mx/developers/es/support
---
## Estado de Implementacion
### Checklist
@ -185,14 +580,5 @@ CREATE TABLE sales.tenant_mercadopago_config (
---
## 9. Referencias
### Documentacion Oficial
- [MercadoPago Developers](https://www.mercadopago.com.mx/developers/es)
- [API Reference](https://www.mercadopago.com.mx/developers/es/reference)
- [SDK Node.js](https://github.com/mercadopago/sdk-nodejs)
---
**Ultima actualizacion:** 2026-01-10
**Ultima actualizacion:** 2026-01-17
**Estado:** PENDIENTE DE IMPLEMENTACION

View File

@ -6,8 +6,8 @@ provider: "Clip México"
status: Mock
integration_type: "Pagos con tarjeta"
created_at: 2026-01-10
updated_at: 2026-01-10
simco_version: "3.8.0"
updated_at: 2026-01-17
simco_version: "4.0.1"
tags:
- pagos
- tarjeta
@ -34,13 +34,13 @@ tags:
## 1. Descripcion
Integración con el sistema de terminales punto de venta (TPV) de Clip México para procesar pagos con tarjeta de crédito y débito. Clip es una de las soluciones de pago más populares en México para pequeños comercios, permitiendo aceptar pagos con tarjeta sin necesidad de una cuenta bancaria empresarial tradicional.
Integracion con el sistema de terminales punto de venta (TPV) de Clip Mexico para procesar pagos con tarjeta de credito y debito. Clip es una de las soluciones de pago mas populares en Mexico para pequenos comercios, permitiendo aceptar pagos con tarjeta sin necesidad de una cuenta bancaria empresarial tradicional.
**Casos de uso principales:**
- Cobro presencial con tarjeta de crédito/débito en el changarro
- Generación de links de pago para cobros a distancia
- Consulta de historial de transacciones y conciliación
- Gestión de reembolsos y cancelaciones
- Cobro presencial con tarjeta de credito/debito en el changarro
- Generacion de links de pago para cobros a distancia
- Consulta de historial de transacciones y conciliacion
- Gestion de reembolsos y cancelaciones
- Reportes de ventas por periodo
---
@ -53,7 +53,7 @@ Integración con el sistema de terminales punto de venta (TPV) de Clip México p
|----------|-------------|------|-------------|
| CLIP_API_KEY | Llave de API proporcionada por Clip | string | SI |
| CLIP_SECRET_KEY | Llave secreta para firmar requests | string | SI |
| CLIP_MERCHANT_ID | Identificador único del comercio en Clip | string | SI |
| CLIP_MERCHANT_ID | Identificador unico del comercio en Clip | string | SI |
| CLIP_WEBHOOK_SECRET | Secret para validar webhooks de Clip | string | SI |
| CLIP_ENVIRONMENT | Ambiente (sandbox/production) | string | SI |
@ -68,6 +68,14 @@ CLIP_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
CLIP_ENVIRONMENT=sandbox
```
### Obtencion de Credenciales
1. Crear cuenta en [Clip Dashboard](https://dashboard.clip.mx/)
2. Acceder a la seccion de Desarrolladores
3. Generar API Key y Secret Key
4. Obtener Merchant ID de la configuracion de cuenta
5. Configurar Webhook URL y obtener Webhook Secret
---
## 3. Arquitectura
@ -95,10 +103,10 @@ CLIP_ENVIRONMENT=sandbox
### Flujo de Pago
1. Usuario selecciona "Pagar con tarjeta" en el punto de venta
2. Backend crea una sesión de pago en Clip API
2. Backend crea una sesion de pago en Clip API
3. Se genera un link/QR para completar el pago
4. Cliente realiza el pago con su tarjeta
5. Clip envía webhook de confirmación
5. Clip envia webhook de confirmacion
6. Backend actualiza estado de la venta
---
@ -107,32 +115,475 @@ CLIP_ENVIRONMENT=sandbox
### Endpoints Consumidos (Clip API)
| Método | Endpoint | Descripción |
| Metodo | Endpoint | Descripcion |
|--------|----------|-------------|
| POST | `/v1/payments` | Crear un nuevo pago |
| GET | `/v1/payments/{id}` | Consultar estado de pago |
| POST | `/v1/payments/{id}/refund` | Procesar reembolso |
| GET | `/v1/transactions` | Listar transacciones |
| GET | `/v1/merchants/{id}/balance` | Consultar balance |
| POST | `/v1/payment-links` | Crear link de pago |
| GET | `/v1/payment-links/{id}` | Consultar link de pago |
### Endpoints Expuestos (MiChangarrito)
| Método | Endpoint | Descripción |
| Metodo | Endpoint | Descripcion |
|--------|----------|-------------|
| POST | `/api/v1/payments/clip/create` | Iniciar pago con Clip |
| GET | `/api/v1/payments/clip/{id}` | Consultar pago |
| POST | `/api/v1/payments/clip/{id}/refund` | Solicitar reembolso |
| POST | `/api/v1/payments/clip/payment-link` | Crear link de pago |
| POST | `/api/v1/webhooks/clip` | Recibir notificaciones de Clip |
---
## 5. Notas de Implementacion
## 5. Rate Limits
- La integración requiere cuenta de desarrollador en Clip Portal
- Clip cobra comisión del 3.6% + IVA por transacción
- Los fondos se depositan en 24-48 horas hábiles
| Limite | Valor | Periodo | Accion si excede |
|--------|-------|---------|------------------|
| Requests API | 100 | por minuto | 429 + Retry-After |
| Crear pagos | 60 | por minuto | 429 + backoff |
| Consultas | 200 | por minuto | 429 + Retry-After |
| Webhooks | Sin limite | - | N/A |
### Estrategia de Retry
```typescript
const RETRY_DELAYS = [1000, 2000, 4000, 8000];
async function executeWithRetry<T>(
operation: () => Promise<T>,
maxRetries = 4
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (error.response?.status === 429 && attempt < maxRetries - 1) {
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter
? parseInt(retryAfter) * 1000
: RETRY_DELAYS[attempt];
await sleep(delay);
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
```
---
## 6. Manejo de Errores
### Codigos de Error
| Codigo | Descripcion | Accion Recomendada | Retry |
|--------|-------------|-------------------|-------|
| 400 | Request invalido | Validar parametros enviados | NO |
| 401 | Credenciales invalidas | Verificar API Key y Secret | NO |
| 402 | Pago rechazado | Informar al usuario, ofrecer alternativa | NO |
| 403 | Operacion no permitida | Verificar permisos del merchant | NO |
| 404 | Recurso no encontrado | Verificar ID de pago/transaccion | NO |
| 429 | Rate limit excedido | Esperar y reintentar con backoff | SI |
| 500 | Error interno de Clip | Reintentar con backoff exponencial | SI |
### Ejemplo de Manejo
```typescript
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
@Injectable()
export class ClipService {
private readonly logger = new Logger(ClipService.name);
async createPayment(params: CreatePaymentDto, tenantId: string): Promise<ClipPayment> {
try {
const response = await this.clipClient.post('/v1/payments', params);
this.logger.log('Clip payment created', {
service: 'clip',
operation: 'createPayment',
paymentId: response.data.id,
amount: params.amount,
tenantId,
});
return response.data;
} catch (error) {
const clipError = error.response?.data;
this.logger.error('Clip payment failed', {
service: 'clip',
operation: 'createPayment',
code: clipError?.code || error.response?.status,
message: clipError?.message || error.message,
tenantId,
});
switch (error.response?.status) {
case 400:
throw new BadRequestException(clipError?.message || 'Parametros de pago invalidos');
case 401:
throw new Error('Credenciales de Clip invalidas');
case 402:
throw new BadRequestException('Pago rechazado: ' + clipError?.decline_reason);
case 429:
// Encolar para reintento
await this.queue.add('clip-payment-retry', { params, tenantId });
throw new Error('Servicio ocupado, reintentando...');
default:
throw error;
}
}
}
}
```
---
## 7. Fallbacks
### Estrategia de Fallback
| Escenario | Fallback | Tiempo maximo |
|-----------|----------|---------------|
| Clip API caida | Encolar pago para reintento | 24 horas |
| Rate limited | Throttle + cola prioritaria | 1 hora |
| Pago rechazado | Ofrecer link de pago alternativo | Inmediato |
| Timeout | Verificar estado y reintentar | 3 intentos |
### Modo Degradado
Si Clip no esta disponible:
- Pagos presenciales se encolan en Redis para reintento
- Usuario puede optar por efectivo y registrar manualmente
- Links de pago existentes siguen funcionando
- Notificacion a admin sobre estado del servicio
```typescript
async createPaymentWithFallback(
params: CreatePaymentDto,
tenantId: string
): Promise<PaymentResult> {
try {
return await this.createPayment(params, tenantId);
} catch (error) {
if (this.isClipUnavailable(error)) {
// Encolar para reintento posterior
const jobId = await this.queue.add('clip-payment-pending', {
params,
tenantId,
createdAt: new Date().toISOString(),
});
return {
status: 'pending_retry',
jobId,
message: 'Pago encolado, se procesara cuando Clip este disponible',
};
}
throw error;
}
}
```
---
## 8. Multi-tenant
### Modelo de Credenciales
- [x] **Por Tenant:** Cada tenant puede configurar sus propias credenciales Clip
- [x] **Global con fallback:** Credenciales compartidas si tenant no tiene propias
### Almacenamiento
```sql
-- En schema payments
CREATE TABLE payments.tenant_clip_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES auth.tenants(id) NOT NULL,
api_key TEXT NOT NULL, -- Encriptado con AES-256
secret_key TEXT NOT NULL, -- Encriptado con AES-256
merchant_id VARCHAR(50) NOT NULL,
webhook_secret TEXT, -- Encriptado
environment VARCHAR(20) DEFAULT 'sandbox',
commission_rate DECIMAL(5,4) DEFAULT 0.0360, -- 3.6% default
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(tenant_id)
);
-- Indice para busqueda rapida
CREATE INDEX idx_tenant_clip_config_tenant ON payments.tenant_clip_config(tenant_id);
```
### Contexto en Llamadas
```typescript
@Injectable()
export class ClipService {
async getClientForTenant(tenantId: string): Promise<ClipClient> {
// Buscar configuracion del tenant
const config = await this.configRepo.findOne({ where: { tenantId } });
if (config?.is_active) {
return new ClipClient({
apiKey: this.decrypt(config.api_key),
secretKey: this.decrypt(config.secret_key),
merchantId: config.merchant_id,
environment: config.environment,
});
}
// Fallback a credenciales globales
return new ClipClient({
apiKey: this.configService.get('CLIP_API_KEY'),
secretKey: this.configService.get('CLIP_SECRET_KEY'),
merchantId: this.configService.get('CLIP_MERCHANT_ID'),
environment: this.configService.get('CLIP_ENVIRONMENT'),
});
}
async createPaymentForTenant(
tenantId: string,
params: CreatePaymentDto
): Promise<ClipPayment> {
const client = await this.getClientForTenant(tenantId);
return client.payments.create(params);
}
}
```
---
## 9. Webhooks
### Endpoints Registrados
| Evento | Endpoint Local | Descripcion |
|--------|----------------|-------------|
| `payment.created` | `/webhooks/clip` | Pago creado |
| `payment.approved` | `/webhooks/clip` | Pago aprobado |
| `payment.declined` | `/webhooks/clip` | Pago rechazado |
| `payment.refunded` | `/webhooks/clip` | Pago reembolsado |
| `payment_link.paid` | `/webhooks/clip` | Link de pago completado |
### Validacion HMAC de Firma
```typescript
import * as crypto from 'crypto';
function verifyClipWebhookSignature(
payload: string,
signature: string,
webhookSecret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', webhookSecret)
.update(payload, 'utf8')
.digest('hex');
// Comparacion segura contra timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Controller
@Post('/webhooks/clip')
async handleClipWebhook(
@Body() body: any,
@Headers('X-Clip-Signature') signature: string,
@Req() request: Request
): Promise<{ received: boolean }> {
const rawBody = (request as any).rawBody;
// Determinar webhook secret (puede ser por tenant)
const webhookSecret = await this.getWebhookSecret(body.merchant_id);
if (!verifyClipWebhookSignature(rawBody, signature, webhookSecret)) {
throw new UnauthorizedException('Invalid webhook signature');
}
await this.processWebhookEvent(body);
return { received: true };
}
```
### Configuracion en Clip Dashboard
1. Acceder a Clip Dashboard > Desarrolladores > Webhooks
2. Agregar endpoint: `https://api.michangarrito.com/webhooks/clip`
3. Seleccionar eventos: `payment.*`, `payment_link.*`
4. Guardar y copiar el Webhook Secret
5. Configurar en variable de entorno `CLIP_WEBHOOK_SECRET`
---
## 10. Testing
### Modo Sandbox
| Ambiente | Credenciales | Comportamiento |
|----------|--------------|----------------|
| Sandbox | `clip_api_test_*` | Pagos simulados, sin cargos reales |
| Production | `clip_api_live_*` | Pagos reales con tarjetas |
### Tarjetas de Prueba Clip
```
# Pago exitoso
4242 4242 4242 4242 Exp: 12/28 CVV: 123
# Pago rechazado - Fondos insuficientes
4000 0000 0000 0002 Exp: 12/28 CVV: 123
# Pago rechazado - Tarjeta robada
4000 0000 0000 0069 Exp: 12/28 CVV: 123
# Requiere autenticacion 3D Secure
4000 0027 6000 3184 Exp: 12/28 CVV: 123
```
### Comandos de Test
```bash
# Test crear pago
curl -X POST https://api-sandbox.clip.mx/v1/payments \
-H "Authorization: Bearer ${CLIP_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"amount": 150.00,
"currency": "MXN",
"description": "Venta de prueba MiChangarrito",
"reference": "test-order-001"
}'
# Test consultar pago
curl -X GET "https://api-sandbox.clip.mx/v1/payments/${PAYMENT_ID}" \
-H "Authorization: Bearer ${CLIP_API_KEY}"
# Test crear link de pago
curl -X POST https://api-sandbox.clip.mx/v1/payment-links \
-H "Authorization: Bearer ${CLIP_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"amount": 299.00,
"currency": "MXN",
"description": "Pedido a domicilio",
"expires_at": "2026-01-20T23:59:59Z"
}'
# Test webhook local (simular evento)
curl -X POST http://localhost:3143/webhooks/clip \
-H "Content-Type: application/json" \
-H "X-Clip-Signature: $(echo -n '{"event":"payment.approved","data":{"id":"pay_123"}}' | openssl dgst -sha256 -hmac ${CLIP_WEBHOOK_SECRET})" \
-d '{"event":"payment.approved","data":{"id":"pay_123","amount":150.00,"status":"approved"}}'
```
---
## 11. Monitoreo
### Metricas a Monitorear
| Metrica | Descripcion | Alerta |
|---------|-------------|--------|
| Latencia API | Tiempo de respuesta Clip API | > 5s |
| Error Rate | % de requests fallidos | > 5% |
| Decline Rate | % de pagos rechazados | > 15% |
| Webhook Delay | Tiempo entre pago y webhook | > 30s |
| Success Rate | % de pagos exitosos | < 85% |
| Daily Volume | Volumen diario en MXN | Anomalia |
### Logs Estructurados
```typescript
// Pago exitoso
this.logger.info('Clip payment successful', {
service: 'clip',
operation: 'createPayment',
tenantId: context.tenantId,
paymentId: result.id,
amount: params.amount,
currency: 'MXN',
duration: durationMs,
});
// Pago rechazado
this.logger.warn('Clip payment declined', {
service: 'clip',
operation: 'createPayment',
tenantId: context.tenantId,
declineCode: error.decline_code,
declineReason: error.decline_reason,
amount: params.amount,
});
// Webhook recibido
this.logger.info('Clip webhook received', {
service: 'clip',
operation: 'webhook',
event: payload.event,
paymentId: payload.data.id,
merchantId: payload.merchant_id,
});
```
### Dashboard Recomendado
Configurar en Grafana/DataDog:
- Grafica de volumen de pagos por hora
- Grafica de tasa de exito/rechazo
- Alertas de latencia y errores
- Resumen de comisiones acumuladas
---
## 12. Referencias
### Documentacion Oficial Clip
- [Clip API Documentation](https://developer.clip.mx/docs)
- [Clip Dashboard](https://dashboard.clip.mx/)
- [Webhooks Reference](https://developer.clip.mx/docs/webhooks)
- [Tarjetas de Prueba](https://developer.clip.mx/docs/testing)
### Informacion de Clip Mexico
- Comision estandar: 3.6% + IVA por transaccion
- Depositos: 24-48 horas habiles
- Soporte: soporte@clip.mx
- Telefono: 55 4162 5252
### Modulos Relacionados
- [Payments Module](../../apps/backend/src/modules/payments/)
- [Sales Module](../../apps/backend/src/modules/sales/)
- [Arquitectura Multi-Tenant](../90-transversal/ARQUITECTURA-MULTI-TENANT-INTEGRACIONES.md)
### Integraciones Relacionadas
- [INT-002: Stripe](./INT-002-stripe.md) - Suscripciones de MiChangarrito
- [INT-004: MercadoPago](./INT-004-mercadopago.md) - Alternativa de pagos
- [INT-006: CoDi Banxico](./INT-006-codi-banxico.md) - Pagos QR bancarios
---
## 13. Notas de Implementacion
- La integracion requiere cuenta de desarrollador en Clip Portal
- Clip cobra comision del 3.6% + IVA por transaccion
- Los fondos se depositan en 24-48 horas habiles
- Implementar idempotency keys para evitar cobros duplicados
- Validar firma HMAC en todos los webhooks recibidos
- Manejar los estados de pago: pending, approved, declined, refunded
- Considerar límites de rate limiting de la API (100 req/min)
- Considerar limites de rate limiting de la API (100 req/min)
- En ambiente sandbox usar tarjetas de prueba proporcionadas por Clip
---
**Ultima actualizacion:** 2026-01-17
**Autor:** Backend Team

View File

@ -6,8 +6,8 @@ provider: "Banxico/STP"
status: Mock
integration_type: "Pagos QR instantáneos"
created_at: 2026-01-10
updated_at: 2026-01-10
simco_version: "3.8.0"
updated_at: 2026-01-17
simco_version: "4.0.1"
tags:
- pagos
- codi
@ -136,15 +136,687 @@ CODI_API_KEY=codi_key_xxxxxxxx
---
## 5. Notas de Implementacion
## 5. Rate Limits
- CoDi no cobra comisiones por transacción (beneficio vs tarjetas)
### 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 códigos QR
- Implementar timeout de 5 minutos para codigos QR
- Validar firma digital en todas las notificaciones de STP
- El registro como participante CoDi requiere trámite con Banxico
- Generar referencia única por transacción para conciliación
- Los QR deben cumplir con el estándar EMVCo para CoDi
- Horario de operación SPEI: 24/7, pero cortes contables a las 17:30
- Considerar implementar caché de CLABEs validadas
- Manejar reintentos en caso de falla de comunicación con 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