---
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)
