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>
424 lines
15 KiB
Markdown
424 lines
15 KiB
Markdown
---
|
|
id: "RF-PAY-003"
|
|
title: "Sistema de Wallet Interno"
|
|
type: "Requirement"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-005"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "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](../_MAP.md)
|
|
|
|
---
|
|
|
|
## 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:**
|
|
```typescript
|
|
@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:**
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```env
|
|
# 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
|
|
|
|
- [ET-PAY-003: Wallet System](../especificaciones/ET-PAY-003-wallet.md)
|
|
|
|
## Historias de Usuario Relacionadas
|
|
|
|
- [US-PAY-010: Ver Historial de Wallet](../historias-usuario/US-PAY-010-ver-historial.md)
|
|
- [US-PAY-005: Comprar Curso](../historias-usuario/US-PAY-005-comprar-curso.md)
|