trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-003-wallet.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

15 KiB

id title type status priority epic project version created_date updated_date
RF-PAY-003 Sistema de Wallet Interno Requirement Done Alta OQI-005 trading-platform 1.0.0 2025-12-05 2026-01-04

RF-PAY-003: Sistema de Wallet Interno

Version: 1.0.0 Fecha: 2025-12-05 Estado: 📋 Planificado Prioridad: P1 (Alta) Story Points: 8 Épica: OQI-005


Descripción

El sistema debe proporcionar un wallet virtual interno donde los usuarios puedan mantener saldo en USD para realizar compras rápidas sin necesidad de ingresar tarjeta en cada transacción, facilitando microtransacciones y mejorando UX.


Objetivo de Negocio

  • Reducir fricción en compras recurrentes
  • Aumentar conversión en cursos de bajo precio
  • Habilitar sistema de recompensas y créditos
  • Generar float (saldo retenido genera interés)
  • Facilitar reembolsos sin devolver a tarjeta

Casos de Uso

  1. Recarga de Wallet: Usuario agrega $50 USD a su wallet
  2. Compra con Wallet: Usuario compra curso de $29 USD usando saldo
  3. Combinación de Métodos: Wallet ($20) + Tarjeta ($9) para compra de $29
  4. Créditos Promocionales: Sistema otorga $10 USD de regalo
  5. Reembolso a Wallet: Devolución de compra va a wallet

Requisitos Funcionales

RF-PAY-003.1: Creación de Wallet

DEBE:

  1. Crear wallet automáticamente al registrarse usuario
  2. Balance inicial: $0.00 USD
  3. Asociar wallet a userId (relación 1:1)
  4. Generar identificador único walletId
  5. Estado inicial: active

Modelo de datos:

@Entity({ name: 'wallets', schema: 'billing' })
class Wallet {
  id: string;              // UUID
  userId: string;          // FK a users (UNIQUE)
  balance: Decimal;        // Saldo disponible en USD
  currency: string;        // USD
  status: WalletStatus;    // active | suspended | closed
  createdAt: Date;
  updatedAt: Date;
}

RF-PAY-003.2: Recarga de Wallet

DEBE:

  1. Permitir recargas de $10 a $500 USD
  2. Procesar recarga via Payment Intent de Stripe
  3. Crear transacción wallet_topup en wallet_transactions
  4. Actualizar balance atómicamente
  5. Enviar confirmación por email

Flujo:

1. Usuario selecciona monto a recargar
2. Backend crea PaymentIntent con metadata.type = 'wallet_topup'
3. Usuario completa pago con Stripe Elements
4. Webhook payment_intent.succeeded dispara:
   - Crear WalletTransaction (type: credit, amount: X)
   - Incrementar Wallet.balance += X
   - Enviar email de confirmación

RF-PAY-003.3: Pago con Wallet

DEBE:

  1. Verificar saldo suficiente antes de compra
  2. Crear transacción course_purchase en wallet_transactions
  3. Decrementar balance atómicamente
  4. Si saldo insuficiente, permitir pago combinado
  5. Registrar compra en payments con paymentMethod = 'wallet'

Validaciones:

  • wallet.balance >= amount
  • wallet.status = 'active'
  • user.status != 'suspended'

RF-PAY-003.4: Pago Combinado (Wallet + Tarjeta)

DEBE:

  1. Calcular monto a pagar con tarjeta: cardAmount = total - walletBalance
  2. Crear PaymentIntent solo por cardAmount
  3. Al confirmar pago:
    • Decrementar wallet.balance (hasta 0)
    • Procesar cardAmount con Stripe
  4. Transacción atómica (rollback si falla tarjeta)

Ejemplo:

Producto: $49 USD
Wallet: $20 USD
Tarjeta: $29 USD

1. Debitar $20 de wallet
2. Crear PaymentIntent de $29
3. Confirmar pago con tarjeta
4. Si falla tarjeta → revertir débito de wallet

RF-PAY-003.5: Historial de Transacciones

DEBE:

  1. Registrar cada movimiento en wallet_transactions
  2. Soportar tipos: credit, debit, refund, admin_adjustment
  3. Incluir metadata descriptiva
  4. Mostrar balance resultante después de cada transacción
  5. Permitir filtrar por tipo y rango de fechas

Modelo:

@Entity({ name: 'wallet_transactions', schema: 'billing' })
class WalletTransaction {
  id: string;              // UUID
  walletId: string;        // FK a wallets
  type: TransactionType;   // credit | debit | refund | admin_adjustment
  amount: Decimal;         // Monto (positivo o negativo)
  balanceBefore: Decimal;  // Balance antes de transacción
  balanceAfter: Decimal;   // Balance después de transacción
  reference: string;       // ID de payment, refund, etc.
  description: string;     // "Compra de curso: Trading Básico"
  metadata?: object;
  createdAt: Date;
}

RF-PAY-003.6: Retiro de Fondos (Withdrawal)

DEBE:

  1. Permitir retiros de mínimo $10 USD
  2. Procesar retiro a cuenta bancaria o tarjeta original
  3. Aplicar fee de procesamiento: $2 USD o 2% (lo mayor)
  4. Tiempo de procesamiento: 5-7 días hábiles
  5. Requerir verificación de identidad (KYC)

Validaciones:

  • wallet.balance >= amount + fee
  • Usuario tiene KYC aprobado
  • Máximo 1 retiro cada 7 días

RF-PAY-003.7: Créditos Promocionales

DEBE:

  1. Permitir admin otorgar créditos a usuarios
  2. Marcar créditos con source: 'promo'
  3. Soportar expiración de créditos (90 días por defecto)
  4. Priorizar uso de créditos no-promocionales primero
  5. Notificar usuario de créditos recibidos

Casos:

  • Registro nuevo: $5 USD de bienvenida
  • Referido: $10 USD por cada amigo invitado
  • Compensación por problema técnico
  • Campaña de marketing

Flujo de Recarga de Wallet

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Usuario   │     │  Frontend   │     │   Backend   │     │   Stripe    │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
       │ Click "Recargar   │                   │                   │
       │ Wallet"           │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │ Ingresa $50       │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │                   │ POST /wallet/topup│                   │
       │                   │ { amount: 50 }    │                   │
       │                   │──────────────────▶│                   │
       │                   │                   │                   │
       │                   │                   │ Validate amount   │
       │                   │                   │ (min/max)         │
       │                   │                   │                   │
       │                   │                   │ Create            │
       │                   │                   │ PaymentIntent     │
       │                   │                   │──────────────────▶│
       │                   │                   │◀──────────────────│
       │                   │                   │ clientSecret      │
       │                   │                   │                   │
       │                   │◀──────────────────│                   │
       │                   │ { clientSecret }  │                   │
       │                   │                   │                   │
       │◀──────────────────│                   │                   │
       │ Muestra Stripe    │                   │                   │
       │ Elements          │                   │                   │
       │                   │                   │                   │
       │ Confirma pago     │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │                   │ confirmPayment()  │                   │
       │                   │──────────────────────────────────────▶│
       │                   │◀──────────────────────────────────────│
       │                   │ success           │                   │
       │                   │                   │                   │
       │                   │                   │◀──────────────────│
       │                   │                   │ Webhook:          │
       │                   │                   │ payment_intent.   │
       │                   │                   │ succeeded         │
       │                   │                   │                   │
       │                   │                   │ BEGIN TX          │
       │                   │                   │ 1. Create         │
       │                   │                   │    WalletTx       │
       │                   │                   │ 2. wallet.balance │
       │                   │                   │    += 50          │
       │                   │                   │ COMMIT TX         │
       │                   │                   │                   │
       │                   │                   │ Send email        │
       │                   │                   │                   │
       │◀──────────────────│                   │                   │
       │ "Recarga exitosa" │                   │                   │
       │ Balance: $50.00   │                   │                   │
       │                   │                   │                   │

Reglas de Negocio

RN-001: Límites de Wallet

Límite Valor
Balance mínimo $0.00 USD
Balance máximo $2,000.00 USD
Recarga mínima $10.00 USD
Recarga máxima $500.00 USD
Retiro mínimo $10.00 USD
Fee de retiro $2.00 o 2% (mayor)

RN-002: Orden de Uso de Fondos

Al realizar pago, usar fondos en este orden:

  1. Créditos promocionales (primero los próximos a expirar)
  2. Saldo regular del wallet
  3. Método de pago externo (tarjeta)

RN-003: Expiración de Créditos

  • Créditos promocionales expiran en 90 días
  • Email de recordatorio 7 días antes de expirar
  • Créditos expirados se eliminan automáticamente
  • Saldo regular nunca expira

RN-004: Reembolsos

Compra pagada con wallet:

  • Reembolso va 100% al wallet (no a tarjeta)
  • Se crea WalletTransaction de tipo refund

Compra pagada con método mixto:

  • Reembolso proporcional:
    • walletAmount → wallet
    • cardAmount → tarjeta (via Stripe Refund)

Estados de Wallet

Estado Descripción Permite Recarga Permite Compra
active Wallet operativo
suspended Suspendido por admin (fraude)
closed Cerrado por usuario

Seguridad

Concurrencia

  • Usar transacciones atómicas para actualizar balance
  • Lock optimista con version column
  • Retry automático si hay conflict
await db.transaction(async (tx) => {
  const wallet = await tx.wallet.findOne({ userId }, { lock: true });
  if (wallet.balance < amount) throw new Error('Insufficient funds');

  await tx.walletTransaction.create({
    walletId: wallet.id,
    type: 'debit',
    amount: -amount,
    balanceBefore: wallet.balance,
    balanceAfter: wallet.balance - amount,
  });

  await tx.wallet.update({ id: wallet.id }, {
    balance: wallet.balance - amount
  });
});

Auditoría

  • Registrar todas las transacciones sin excepción
  • Logs detallados de cambios de balance
  • Alertas automáticas si:
    • Balance negativo (imposible pero detectar)
    • Transacciones > $500 en 1 hora
    • Más de 10 compras en 1 día

Prevención de Fraude

  • Limite de $500 USD en recargas diarias
  • Verificar patrón de uso normal
  • Bloquear wallet si se detecta actividad sospechosa
  • Requerir KYC para retiros

Configuración Requerida

# Wallet Limits
WALLET_MIN_BALANCE=0
WALLET_MAX_BALANCE=2000
WALLET_TOPUP_MIN=10
WALLET_TOPUP_MAX=500
WALLET_WITHDRAWAL_MIN=10
WALLET_WITHDRAWAL_FEE_FIXED=2.00
WALLET_WITHDRAWAL_FEE_PERCENT=0.02

# Promo Credits
PROMO_CREDIT_EXPIRATION_DAYS=90
PROMO_CREDIT_REMINDER_DAYS=7

Manejo de Errores

Error Código Mensaje Usuario
Saldo insuficiente 400 Saldo insuficiente. Tienes $X, necesitas $Y.
Wallet suspendido 403 Tu wallet está suspendido. Contacta soporte.
Límite de balance 400 No puedes tener más de $2,000 en tu wallet.
Recarga muy pequeña 400 El monto mínimo de recarga es $10 USD.
Retiro sin KYC 403 Verifica tu identidad para retirar fondos.

Webhooks Relacionados

Evento Acción
payment_intent.succeeded (wallet_topup) Incrementar balance
refund.created Incrementar balance si corresponde

Métricas de Negocio

KPIs a Rastrear

  • Total Float: Suma de todos los balances de wallets
  • Avg Wallet Balance: Balance promedio por usuario
  • Topup Conversion Rate: % de usuarios que recargan
  • Wallet Usage Rate: % de compras pagadas con wallet
  • Promo Credit ROI: Conversión de créditos promocionales

Criterios de Aceptación

  • Wallet se crea automáticamente al registrarse
  • Usuario puede recargar saldo con tarjeta
  • Usuario puede pagar curso con saldo de wallet
  • Sistema valida saldo suficiente antes de compra
  • Pago combinado (wallet + tarjeta) funciona correctamente
  • Historial de transacciones muestra todos los movimientos
  • Créditos promocionales se aplican automáticamente
  • Créditos promocionales expiran correctamente
  • Retiros a cuenta bancaria funcionan (con fee)
  • Reembolsos se acreditan al wallet correctamente
  • Transacciones son atómicas (no hay estados inconsistentes)

Especificación Técnica Relacionada

Historias de Usuario Relacionadas