--- id: "RF-PAY-008" title: "Sistema de Depositos via SPEI" type: "Requirement" status: "Draft" priority: "Alta" epic: "OQI-005" project: "trading-platform" version: "1.0.0" created_date: "2026-01-04" updated_date: "2026-01-04" --- # RF-PAY-008: Sistema de Depositos via SPEI **Version:** 1.0.0 **Fecha:** 2026-01-04 **Estado:** Draft **Prioridad:** P1 (Alta) **Story Points:** 8 **Epica:** [OQI-005](../_MAP.md) --- ## Descripcion El sistema debe permitir a los usuarios en Mexico realizar depositos utilizando SPEI (Sistema de Pagos Electronicos Interbancarios), el sistema de transferencias bancarias instantaneas del pais. Se integrara con un agregador de pagos (Stripe Mexico, Conekta u OpenPay) para generar CLABEs virtuales unicas por usuario y reconciliar automaticamente los depositos recibidos. --- ## Objetivo de Negocio - Capturar mercado mexicano con metodo de pago preferido (85% de transferencias) - Reducir costos de transaccion vs tarjetas (1-2% vs 3.5%) - Eliminar chargebacks (SPEI es irreversible) - Ofrecer alternativa para usuarios sin tarjeta de credito - Aumentar ticket promedio (SPEI permite montos mayores) --- ## Casos de Uso 1. **Deposito Simple:** Usuario transfiere $5,000 MXN desde su banca en linea a su CLABE virtual 2. **Primer Deposito:** Usuario obtiene su CLABE unica y realiza primera transferencia 3. **Deposito Recurrente:** Usuario guarda CLABE como beneficiario frecuente en su banco 4. **Reconciliacion:** Sistema detecta deposito y acredita USD equivalente automaticamente 5. **Notificacion:** Usuario recibe push/email confirmando deposito procesado --- ## Integracion con Agregadores ### Opciones de Agregador | Agregador | CLABE Virtual | Fee | Tiempo Integracion | |-----------|---------------|-----|---------------------| | Stripe MX | Si | 1% + $3 MXN | 2-3 semanas | | Conekta | Si | 1.2% + $4 MXN | 1-2 semanas | | OpenPay | Si | 0.9% + $2.5 MXN | 2-3 semanas | **Recomendado:** Stripe MX por consistencia con otros metodos de pago. --- ## Requisitos Funcionales ### RF-PAY-008.1: Generacion de CLABE Virtual **DEBE:** 1. Generar CLABE de 18 digitos unica por usuario 2. Asociar CLABE a `userId` en tabla `spei_references` 3. CLABE permanente (no expira) 4. Mostrar en UI con opcion de copiar 5. Incluir nombre del beneficiario estandarizado **Formato CLABE:** ``` Banco (3) + Plaza (3) + Cuenta (11) + Digito verificador (1) Ejemplo: 646180157000001234 ^^^ Banco: STP (646) ^^^ Plaza: 180 ^^^^^^^^^^^ Cuenta unica por usuario ^ Digito verificador ``` **Modelo de datos:** ```typescript @Entity({ name: 'spei_references', schema: 'billing' }) class SpeiReference { id: string; // UUID userId: string; // FK a users (UNIQUE) walletId: string; // FK a wallets clabe: string; // CLABE 18 digitos (UNIQUE) beneficiaryName: string; // "TRADING PLATFORM/USER123" bankName: string; // "STP" o banco del agregador isActive: boolean; // true createdAt: Date; updatedAt: Date; } ``` ### RF-PAY-008.2: Recepcion de Depositos **DEBE:** 1. Recibir webhook del agregador cuando llega transferencia 2. Validar firma del webhook 3. Identificar usuario por CLABE destino 4. Crear registro en `spei_deposits` 5. Procesar conversion y acreditacion **Datos del webhook:** ```json { "event": "spei.incoming_transfer", "data": { "id": "tr_xxx", "clabe_destino": "646180157000001234", "clabe_origen": "012180001234567890", "monto": 5000.00, "concepto": "DEPOSITO TRADING", "referencia_numerica": "1234567", "nombre_ordenante": "JUAN PEREZ", "rfc_ordenante": "XAXX010101000", "fecha_operacion": "2026-01-04T10:30:00Z" } } ``` ### RF-PAY-008.3: Conversion MXN a USD **DEBE:** 1. Obtener tipo de cambio actual MXN/USD 2. Usar tipo de cambio del Banco de Mexico + spread 3. Calcular USD equivalente 4. Acreditar al wallet en USD 5. Registrar tipo de cambio usado **Calculo:** ``` USD_Amount = MXN_Amount / (Exchange_Rate * (1 + spread)) Ejemplo (spread 0.5%): $5,000 MXN / (17.50 * 1.005) = $284.21 USD ``` ### RF-PAY-008.4: Reconciliacion Automatica **DEBE:** 1. Procesar depositos automaticamente (sin intervencion manual) 2. Manejar depositos duplicados (idempotencia) 3. Rechazar depositos de CLABEs no registradas 4. Alertar sobre depositos anomalos (montos muy altos) 5. Generar reporte diario de reconciliacion **Estados de Deposito:** ``` RECEIVED → Webhook recibido, validando PROCESSING → Conversion en proceso COMPLETED → Acreditado al wallet FAILED → Error en procesamiento REJECTED → CLABE no encontrada / limite excedido ``` ### RF-PAY-008.5: Notificaciones **DEBE:** 1. Push notification inmediata al recibir deposito 2. Email de confirmacion con detalles 3. Mostrar en historial de transacciones 4. Incluir tipo de cambio usado **Contenido de notificacion:** ``` Asunto: Deposito SPEI recibido - $5,000 MXN Has recibido un deposito SPEI: - Monto: $5,000.00 MXN - Equivalente: $284.21 USD (TC: 17.59) - Referencia: 1234567 - Fecha: 4 de enero 2026, 10:30 AM Tu nuevo saldo es: $534.21 USD ``` --- ## Modelo de Datos ### Tabla: spei_deposits ```typescript @Entity({ name: 'spei_deposits', schema: 'billing' }) class SpeiDeposit { id: string; // UUID userId: string; // FK a users walletId: string; // FK a wallets speiReferenceId: string; // FK a spei_references status: SpeiDepositStatus; // received, processing, completed, failed, rejected // Datos SPEI clabeOrigen: string; // CLABE del ordenante clabeDestino: string; // CLABE del usuario montoMxn: Decimal; // Monto en MXN concepto: string; // Concepto de la transferencia referenciaNumerica: string; // Referencia numerica (7 digitos) nombreOrdenante: string; // Nombre del que envia rfcOrdenante?: string; // RFC del ordenante fechaOperacion: Date; // Fecha/hora de la operacion // Conversion montoUsd: Decimal; // Monto convertido a USD tipoCambio: Decimal; // Tipo de cambio usado spreadAplicado: Decimal; // Spread aplicado (0.005) // Proveedor providerRef: string; // ID en Stripe/Conekta/OpenPay providerData?: object; // Datos raw del proveedor // Timestamps createdAt: Date; processedAt?: Date; updatedAt: Date; } enum SpeiDepositStatus { RECEIVED = 'received', PROCESSING = 'processing', COMPLETED = 'completed', FAILED = 'failed', REJECTED = 'rejected' } ``` --- ## Flujo de Deposito SPEI ``` +-------------+ +-------------+ +-------------+ +-------------+ +-------------+ | Usuario | | Banco MX | | Agregador | | Backend | | Wallet | +------+------+ +------+------+ +------+------+ +------+------+ +------+------+ | | | | | | 1. Obtener CLABE | | | | |------------------>| | | | | | | | | |<------------------| | | | | CLABE: | | | | | 646180157000001234| | | | | | | | | | 2. Accede a banca | | | | | en linea | | | | |------------------>| | | | | | | | | | 3. Registra CLABE | | | | | como beneficiario | | | | |------------------>| | | | | | | | | | 4. Transfiere | | | | | $5,000 MXN | | | | |------------------>| | | | | | | | | | | 5. SPEI procesa | | | | | transferencia | | | | | (5-30 min) | | | | |------------------>| | | | | | | | | | | 6. Webhook: | | | | | spei.incoming | | | | |------------------>| | | | | | | | | | | 7. Validar CLABE | | | | | en spei_references| | | | | | | | | | 8. Crear | | | | | spei_deposit | | | | | status: RECEIVED | | | | | | | | | | 9. Obtener TC | | | | | del dia | | | | | | | | | | 10. Convertir | | | | | $5000 MXN = | | | | | $284.21 USD | | | | | | | | | | 11. Acreditar | | | | |------------------>| | | | |<------------------| | | | | wallet.balance | | | | | += $284.21 | | | | | | | | | | 12. Update status | | | | | COMPLETED | | | | | | |<------------------|-------------------|-------------------| | | Push: "Deposito | | | | | recibido! | | | | | +$284.21 USD" | | | | | | | | | |<------------------|-------------------|-------------------| | | Email: Detalles | | | | | del deposito | | | | | | | | | ``` --- ## Reglas de Negocio ### RN-001: Limites de Deposito SPEI | Limite | Valor | |--------|-------| | Deposito minimo | $100 MXN | | Deposito maximo por transaccion | $500,000 MXN | | Deposito maximo diario | $1,000,000 MXN | | Sin limite mensual | - | ### RN-002: Tipo de Cambio - Usar tipo de cambio FIX del Banco de Mexico - Actualizar tipo de cambio cada 15 minutos - Aplicar spread de **0.5%** sobre tipo de cambio - Cachear tipo de cambio para consistencia intraday - Mostrar tipo de cambio antes de depositar ### RN-003: Horarios SPEI | Horario | Disponibilidad | |---------|----------------| | Lunes-Viernes 6:00-17:30 | Tiempo real (5-15 min) | | Lunes-Viernes 17:30-20:00 | Mismo dia (antes 20:00) | | Sabados 6:00-14:00 | Tiempo real | | Domingos/Festivos | Siguiente dia habil | ### RN-004: Validacion de Depositos - CLABE destino debe existir en `spei_references` - Monto debe estar dentro de limites - Referencia numerica no debe repetirse (idempotencia) - Depositos fuera de horario se procesan en siguiente ventana ### RN-005: Reconciliacion - Depositos se reconcilian automaticamente por CLABE - No se requiere referencia especifica del usuario - Concepto de transferencia se guarda para auditoria - Reportes diarios a las 20:00 CST --- ## Integracion con Stripe Mexico ### Configuracion ```typescript // Crear cuenta SPEI reference const speiReference = await stripe.customers.createSource( customerId, { type: 'clabe', clabe: { // Stripe genera automaticamente } } ); // Resultado { id: "src_xxx", type: "clabe", clabe: { clabe: "646180157000001234", bank_name: "STP", beneficiary_name: "STRIPE/TRADING PLATFORM" } } ``` ### Webhook Handler ```typescript @Post('/webhooks/stripe/spei') async handleSpeiWebhook( @Body() payload: StripeWebhookPayload, @Headers('stripe-signature') signature: string ) { // 1. Validar firma const event = stripe.webhooks.constructEvent( payload, signature, process.env.STRIPE_SPEI_WEBHOOK_SECRET ); // 2. Procesar segun tipo de evento switch (event.type) { case 'source.chargeable': await this.processSpeiDeposit(event.data.object); break; case 'source.failed': await this.handleSpeiFailure(event.data.object); break; } return { received: true }; } ``` --- ## API Endpoints ### Obtener CLABE ```yaml GET /api/v1/spei/clabe: description: Obtener CLABE virtual del usuario response: clabe: string bankName: string beneficiaryName: string limits: min: number max: number dailyMax: number exchangeRate: rate: number validUntil: string ``` ### Historial de Depositos SPEI ```yaml GET /api/v1/spei/deposits: description: Listar depositos SPEI del usuario query: page: number limit: number status: string dateFrom: string dateTo: string response: data: - id: string status: string montoMxn: number montoUsd: number tipoCambio: number fechaOperacion: string nombreOrdenante: string pagination: total: number page: number pages: number ``` ### Tipo de Cambio Actual ```yaml GET /api/v1/spei/exchange-rate: description: Obtener tipo de cambio MXN/USD actual response: rate: number spread: number effectiveRate: number validUntil: string source: string ``` --- ## Seguridad ### Validacion de Webhooks ```typescript function validateStripeSpeiSignature( payload: string, signature: string, secret: string ): boolean { const expectedSig = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(`sha256=${expectedSig}`) ); } ``` ### Prevencion de Fraude - Monitorear depositos de misma CLABE origen (lavado) - Alerta si deposito > $100,000 MXN - Verificar RFC del ordenante vs usuario registrado - Bloquear cuenta si patron sospechoso ### Idempotencia - Usar `referenciaNumerica` + `fechaOperacion` como clave unica - Ignorar webhooks duplicados - Registrar todos los intentos para auditoria --- ## Manejo de Errores | Error | Codigo | Mensaje Usuario | |-------|--------|-----------------| | CLABE no encontrada | 404 | Error interno. Contacta soporte. | | Monto bajo limite | 400 | El monto minimo de deposito es $100 MXN | | Monto sobre limite | 400 | El monto maximo por transferencia es $500,000 MXN | | Limite diario excedido | 400 | Has excedido el limite diario de $1,000,000 MXN | | Error de conversion | 500 | Error procesando tipo de cambio. Intenta mas tarde. | | Proveedor no disponible | 503 | Sistema SPEI temporalmente no disponible | --- ## Webhooks Recibidos | Evento | Accion | |--------|--------| | `source.chargeable` | Crear spei_deposit, procesar conversion | | `source.failed` | Marcar como FAILED, notificar usuario | | `source.canceled` | Marcar como REJECTED | --- ## Configuracion Requerida ```env # Stripe Mexico STRIPE_MX_SECRET_KEY=sk_xxx STRIPE_SPEI_WEBHOOK_SECRET=whsec_xxx # Conekta (alternativo) CONEKTA_API_KEY=xxx CONEKTA_WEBHOOK_SECRET=xxx # Tipo de Cambio EXCHANGE_RATE_API_URL=https://api.banxico.org.mx/SieAPIRest/service/v1/series/SF43718/datos/oportuno EXCHANGE_RATE_BANXICO_TOKEN=xxx EXCHANGE_RATE_CACHE_TTL_MINUTES=15 EXCHANGE_RATE_SPREAD=0.005 # Limites SPEI SPEI_DEPOSIT_MIN_MXN=100 SPEI_DEPOSIT_MAX_MXN=500000 SPEI_DEPOSIT_MAX_DAILY_MXN=1000000 ``` --- ## Metricas de Negocio ### KPIs a Rastrear - **Depositos SPEI/dia:** Numero de depositos procesados - **Volumen MXN/dia:** Total depositado en MXN - **Volumen USD/dia:** Total convertido a USD - **Ticket promedio:** Monto promedio por deposito - **Tiempo procesamiento:** Promedio entre recepcion y acreditacion - **Tasa de error:** % de depositos fallidos/rechazados --- ## Criterios de Aceptacion - [ ] Usuario obtiene CLABE unica de 18 digitos - [ ] CLABE se muestra con opcion de copiar - [ ] Depositos se detectan via webhook del agregador - [ ] Conversion MXN a USD aplica tipo de cambio correcto - [ ] Spread de 0.5% se aplica correctamente - [ ] Wallet se acredita en USD automaticamente - [ ] Push notification se envia al confirmar deposito - [ ] Email de confirmacion incluye todos los detalles - [ ] Depositos duplicados se manejan correctamente (idempotencia) - [ ] Limites de monto se validan correctamente - [ ] Historial de depositos SPEI es visible en UI - [ ] Tipo de cambio se actualiza cada 15 minutos --- ## Especificacion Tecnica Relacionada - [ET-PAY-008: SPEI Integration](../especificaciones/ET-PAY-008-spei.md) ## Historias de Usuario Relacionadas - [US-PAY-025: Obtener CLABE](../historias-usuario/US-PAY-025-obtener-clabe.md) - [US-PAY-026: Depositar via SPEI](../historias-usuario/US-PAY-026-depositar-spei.md) - [US-PAY-027: Ver Historial SPEI](../historias-usuario/US-PAY-027-historial-spei.md)