1. Arquitectura de Seguridad
Arquitectura de pagos — Stripe Connect Express
Actores
Stripe
nvito-api
nvito-api
PostgreSQL
Stripe
PostgreSQL
Actores
El flujo financiero de Nvito sigue 7 principios fundamentales:
| Principio | Implementación |
|---|---|
| Zero-trust en metadata | Nunca confiar en datos de metadata de Stripe. Siempre verificar contra BD |
| Idempotencia | Toda operación financiera es idempotente (doble ejecución = mismo resultado) |
| Transacciones atómicas | Todo cambio financiero ocurre en una sola transacción Prisma |
| Menor privilegio | Endpoints de Connect solo para admin. Pagos públicos con rate limiting |
| Sin flujo legacy | No existe camino donde dinero pueda caer en la cuenta de Nvito |
| Validación server-side | Todos los límites se validan en el backend. El frontend solo muestra |
| Auditoría completa | Toda transacción registrada con stripePaymentIntentId, applicationFeeAmount |
2. Cadena de Validaciones
Cada contribución pasa por 6 validaciones obligatorias antes de llegar a Stripe:
Cadena de validaciones de seguridad — createCashFundIntent
Validaciones
Procesamiento
| Validación | Dónde | Rechaza si |
|---|---|---|
| Evento ACTIVE | Backend | Evento en DRAFT, COMPLETED o CANCELLED |
| cashFundEnabled | Backend | Sobre de Regalo desactivado |
| Cuenta Connect verificada | Backend | No existe o chargesEnabled = false |
| Límite por contribución | Backend + Client | amount > cashFundMaxContribution |
| Límite del fondo | Backend + Client | cashFundCurrent + amount > cashFundMaxGoal |
| Nombre del pagador | Backend + Client | payerName vacío |
Doble validación
Los límites se validan tanto en el cliente (UX inmediata) como en el backend (seguridad). El frontend valida en PESOS, el backend en CENTAVOS.
3. Flujo de Dinero
Flujo completo de contribucion — Cash Fund via Stripe Connect
Desglose de Comisiones
Desglose de comisiones por contribucion
Cobro
Comisiones
Destino
| Concepto | Porcentaje | Sobre $1,000 MXN |
|---|---|---|
| Stripe México (tarjeta) | ~3.6% + $3 MXN | ~$39 |
| Comisión Nvito (configurable) | 10-15% | $100-$150 |
| Anfitrión recibe | ~82-87% | $811-$861 |
La application_fee_amount se calcula en el backend a partir de giftRegistry.cashFundFeePercent. El frontend NO puede modificarla.
4. Vectores de Ataque y Mitigaciones
4.1 Inyección de stripeAccountId
| Detalle | |
|---|---|
| Riesgo | Dinero va a cuenta equivocada |
| Mitigación | El webhook usa payment.stripeConnectedAccountId de BD, NO de metadata de Stripe |
4.2 PaymentIntent sin cuenta Connect
| Detalle | |
|---|---|
| Riesgo | Dinero cae en cuenta Nvito |
| Mitigación | createCashFundIntent SIEMPRE requiere getVerifiedAccountForEvent(). Sin cuenta verificada = ValidationException. No existe flujo legacy |
4.3 Doble incremento de cashFundCurrent
| Detalle | |
|---|---|
| Riesgo | Conteo inflado |
| Mitigación | Webhook verifica payment.status !== SUCCEEDED antes de incrementar. Prisma.increment es atómico |
4.4 Manipulación de application_fee
| Detalle | |
|---|---|
| Riesgo | Nvito no recibe comisión |
| Mitigación | La fee se calcula en el BACKEND. El frontend no puede modificarla. Stripe la cobra automáticamente |
4.5 Contribución que excede límite (race condition)
| Detalle | |
|---|---|
| Riesgo | Fondo excede meta |
| Mitigación | cashFundMaxGoal es un soft limit validado pre-intent. La validación no usa lock transaccional, por lo que contribuciones simultáneas podrían exceder marginalmente. Aceptable para MVP |
4.6 Webhook falsificado
| Detalle | |
|---|---|
| Riesgo | Procesamiento de pagos falsos |
| Mitigación | Firma validada con constructWebhookEvent(). Connect usa secret separado (STRIPE_CONNECT_WEBHOOK_SECRET) |
4.7 Chargebacks y disputas
| Detalle | |
|---|---|
| Riesgo | Pérdida de dinero e inconsistencia de datos |
| Mitigación | Handler handleDisputeCreated decrementa cashFundCurrent atómicamente y marca Payment como REFUNDED. Las disputas se manejan en la cuenta del organizador, no de Nvito |
4.8 Admin resetea cashFundCurrent
| Detalle | |
|---|---|
| Riesgo | Pérdida de integridad financiera |
| Mitigación | gift-registry.service.ts hace delete data.cashFundCurrent en el update — imposible modificar via DTO. Solo webhook y confirm pueden modificarlo |
4.9 Desactivación con fondos recaudados
| Detalle | |
|---|---|
| Riesgo | Pérdida de datos financieros |
| Mitigación | handleToggle solo envía { cashFundEnabled: false }. NO resetea cashFundCurrent, cashFundGoal ni la cuenta Connect. Confirmación obligatoria si hay fondos |
4.10 Stripe desactiva la cuenta
| Detalle | |
|---|---|
| Riesgo | Invitados ven botón de pago roto |
| Mitigación | Webhook account.updated detecta estado DISABLED → desactiva cashFundEnabled automáticamente + log de alerta |
4.11 Eliminación de evento con fondos
| Detalle | |
|---|---|
| Riesgo | Cuenta Connect huérfana, pagos perdidos |
| Mitigación | El backend bloquea eliminación si hay pagos PROCESSING. Marca cuenta Connect como COMPLETED antes del soft delete |
5. Webhooks Procesados
Webhooks de Stripe y Stripe Connect
| Evento | Handler | Acción | Idempotente |
|---|---|---|---|
payment_intent.succeeded | handlePaymentIntentSucceeded | SUCCEEDED + cashFundCurrent++ | Sí (verifica status previo) |
payment_intent.payment_failed | handlePaymentIntentFailed | FAILED | Sí |
charge.dispute.created | handleDisputeCreated | REFUNDED + cashFundCurrent-- | Sí (verifica status previo) |
charge.refunded | handleChargeRefunded | REFUNDED + cashFundCurrent-- | Sí (verifica status previo) |
account.updated | handleAccountUpdated | Sync status + auto-disable | Sí |
6. Protección de Datos Personales
| Dato | Retención | Anonimización |
|---|---|---|
legalName | Hasta 6 meses post-evento | Se establece a null |
email | Hasta 6 meses post-evento | Se establece a null |
rfc | Hasta 6 meses post-evento | Se establece a null |
stripeAccountId | Permanente | Se mantiene como referencia |
Datos que Nvito NUNCA almacena
- Números de tarjeta de crédito (PCI compliance via Stripe)
- CLABE bancaria (solo en Stripe)
- Documentos de identidad (solo en Stripe)
7. Procedimiento de Respuesta ante Disputas
- Stripe notifica via webhook
charge.dispute.created - El sistema decrementa
cashFundCurrenty marca el Payment como REFUNDED - La disputa se maneja en la cuenta Connect del organizador
- El organizador tiene 7-21 días para responder con evidencia
- Nvito NO interviene financieramente — la disputa es entre invitado y organizador
Importante
Con Stripe Connect Express, Nvito actúa como intermediario tecnológico. Las disputas financieras son responsabilidad del organizador (titular de la cuenta Connect).
8. Ciclo de Vida de la Cuenta Connect
Estados de la cuenta Stripe Connect
not_started→onboardingAdmin crea cuenta(createConnectedAccount)onboarding→verifiedchargesEnabled = true(webhook account.updated)onboarding→restrictedRequisitos pendientes(webhook account.updated)restricted→verifiedDocumentos OK(webhook account.updated)restricted→disabledCuenta rechazada(rejected.fraud)verified→disabledStripe desactiva(rejected.*)verified→completedEvento termina(event-lifecycle)completed→archived6 meses post-evento(cron job)verified→archivedAdmin archiva(archiveAccount)disabled→archivedAdmin archiva(archiveAccount)Cambio de Beneficiario
Solo un Super Admin puede ejecutar el cambio:
- Verifica que no hay pagos PROCESSING
- Archiva la cuenta actual (status → ARCHIVED)
- Los pagos anteriores quedan vinculados a la cuenta archivada
- Se puede crear una nueva cuenta Connect para el mismo evento
9. Archivos Clave
| Archivo | Responsabilidad |
|---|---|
stripe-connect.service.ts | Gestión cuentas (crear, sync, archivar, verificar) |
stripe-connect.controller.ts | Endpoints admin + archivo Super Admin |
payments.service.ts | createCashFundIntent con validaciones |
stripe-webhook.service.ts | Handlers de todos los eventos (payment, dispute, refund) |
payments-webhook.controller.ts | Endpoint webhook Stripe + Connect |
gift-registry.service.ts | Protección cashFundCurrent + validación meta vs recaudado |
event-mutation.service.ts | Protección eliminación evento con pagos |