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>
15 KiB
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
- Recarga de Wallet: Usuario agrega $50 USD a su wallet
- Compra con Wallet: Usuario compra curso de $29 USD usando saldo
- Combinación de Métodos: Wallet ($20) + Tarjeta ($9) para compra de $29
- Créditos Promocionales: Sistema otorga $10 USD de regalo
- Reembolso a Wallet: Devolución de compra va a wallet
Requisitos Funcionales
RF-PAY-003.1: Creación de Wallet
DEBE:
- Crear wallet automáticamente al registrarse usuario
- Balance inicial: $0.00 USD
- Asociar wallet a
userId(relación 1:1) - Generar identificador único
walletId - 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:
- Permitir recargas de $10 a $500 USD
- Procesar recarga via Payment Intent de Stripe
- Crear transacción
wallet_topupenwallet_transactions - Actualizar balance atómicamente
- 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:
- Verificar saldo suficiente antes de compra
- Crear transacción
course_purchaseenwallet_transactions - Decrementar balance atómicamente
- Si saldo insuficiente, permitir pago combinado
- Registrar compra en
paymentsconpaymentMethod = 'wallet'
Validaciones:
wallet.balance >= amountwallet.status = 'active'user.status != 'suspended'
RF-PAY-003.4: Pago Combinado (Wallet + Tarjeta)
DEBE:
- Calcular monto a pagar con tarjeta:
cardAmount = total - walletBalance - Crear PaymentIntent solo por
cardAmount - Al confirmar pago:
- Decrementar
wallet.balance(hasta 0) - Procesar
cardAmountcon Stripe
- Decrementar
- 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:
- Registrar cada movimiento en
wallet_transactions - Soportar tipos:
credit,debit,refund,admin_adjustment - Incluir metadata descriptiva
- Mostrar balance resultante después de cada transacción
- 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:
- Permitir retiros de mínimo $10 USD
- Procesar retiro a cuenta bancaria o tarjeta original
- Aplicar fee de procesamiento: $2 USD o 2% (lo mayor)
- Tiempo de procesamiento: 5-7 días hábiles
- 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:
- Permitir admin otorgar créditos a usuarios
- Marcar créditos con
source: 'promo' - Soportar expiración de créditos (90 días por defecto)
- Priorizar uso de créditos no-promocionales primero
- 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:
- Créditos promocionales (primero los próximos a expirar)
- Saldo regular del wallet
- 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
WalletTransactionde tiporefund
Compra pagada con método mixto:
- Reembolso proporcional:
walletAmount→ walletcardAmount→ 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
versioncolumn - 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)