---
id: "RF-PAY-006"
title: "Sistema de Reembolsos"
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-006: Sistema de Reembolsos

**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** 📋 Planificado
**Prioridad:** P2 (Media)
**Story Points:** 6
**Épica:** [OQI-005](../_MAP.md)

---

## Descripción

El sistema debe permitir procesar reembolsos parciales y totales de pagos completados, tanto iniciados por usuarios como por administradores, cumpliendo con políticas de reembolso y manteniendo auditoría completa de transacciones.

---

## Objetivo de Negocio

- Mejorar satisfacción del cliente con proceso claro
- Cumplir con derecho de desistimiento (14 días en EU/LATAM)
- Reducir disputas y chargebacks
- Mantener reputación de la marca
- Automatizar aprobaciones simples, escalar casos complejos

---

## Tipos de Reembolso

| Tipo | Descripción | Aprobación | Plazo |
|------|-------------|------------|-------|
| **Automático** | Dentro de 7 días, curso no iniciado | Automática | Inmediato |
| **Manual** | Fuera de 7 días o curso en progreso | Admin | 1-3 días hábiles |
| **Parcial** | Devolución de % del monto | Admin | 1-3 días hábiles |
| **Por Disputa** | Chargeback iniciado por banco | Automático | Según banco |

---

## Requisitos Funcionales

### RF-PAY-006.1: Solicitud de Reembolso por Usuario

**DEBE:**
1. Permitir solicitar reembolso desde historial de pagos
2. Mostrar elegibilidad según políticas
3. Solicitar motivo (lista predefinida + campo libre)
4. Confirmar acción con advertencia de consecuencias
5. Enviar email de confirmación de solicitud

**Motivos de Reembolso:**
```typescript
enum RefundReason {
  NOT_AS_DESCRIBED = 'not_as_described',       // Producto no es como se describió
  ACCIDENTAL_PURCHASE = 'accidental_purchase', // Compra accidental
  DUPLICATE = 'duplicate',                      // Pago duplicado
  TECHNICAL_ISSUE = 'technical_issue',          // Problema técnico
  DISSATISFIED = 'dissatisfied',                // No satisfecho con el contenido
  OTHER = 'other',                              // Otro (requiere descripción)
}
```

### RF-PAY-006.2: Validación de Elegibilidad

**DEBE validar:**
1. Pago existe y status = `succeeded`
2. No se ha reembolsado previamente
3. Dentro de ventana de reembolso permitida
4. Usuario es el propietario del pago
5. Producto/servicio es reembolsable

**Reglas de elegibilidad:**
```typescript
function isRefundEligible(payment: Payment): boolean {
  // 1. Pago exitoso
  if (payment.status !== 'succeeded') return false;

  // 2. No reembolsado previamente
  if (payment.refundedAt) return false;

  // 3. Dentro de ventana de reembolso
  const daysSincePurchase = daysBetween(payment.createdAt, new Date());

  if (payment.type === 'course_purchase') {
    // Curso: 7 días si no ha iniciado
    const courseProgress = await getCourseProgress(payment.userId, payment.courseId);
    if (courseProgress === 0 && daysSincePurchase <= 7) return true;

    // 14 días con progreso < 20%
    if (courseProgress < 20 && daysSincePurchase <= 14) return true;

    return false; // Fuera de ventana
  }

  if (payment.type === 'subscription') {
    // Suscripción: NO reembolsable (solo cancelación)
    return false;
  }

  return true;
}
```

### RF-PAY-006.3: Aprobación Automática vs Manual

**Aprobación Automática** (procesamiento inmediato):
- Pago < 7 días
- Curso con progreso = 0%
- Monto < $50 USD
- Usuario sin historial de reembolsos abusivos

**Aprobación Manual** (requiere revisión admin):
- Pago > 7 días
- Curso con progreso > 0%
- Monto > $50 USD
- Usuario con > 2 reembolsos en 30 días

### RF-PAY-006.4: Procesamiento de Reembolso

**Backend DEBE:**
1. Crear registro en `billing.refunds`
2. Llamar Stripe API: `stripe.refunds.create()`
3. Revocar acceso al recurso (curso, suscripción)
4. Actualizar `payment.status = 'refunded'`
5. Generar nota de crédito (factura)
6. Enviar email de confirmación

**Implementación:**
```typescript
async function processRefund(refundRequest: RefundRequest): Promise<Refund> {
  const payment = await db.payment.findOne({ id: refundRequest.paymentId });

  await db.transaction(async (tx) => {
    // 1. Crear Refund en Stripe
    const stripeRefund = await stripe.refunds.create({
      payment_intent: payment.stripePaymentIntentId,
      amount: refundRequest.amount, // En centavos (null = total)
      reason: refundRequest.reason,
      metadata: {
        userId: payment.userId,
        refundRequestId: refundRequest.id,
      }
    });

    // 2. Guardar en BD
    const refund = await tx.refund.create({
      id: uuid(),
      paymentId: payment.id,
      userId: payment.userId,
      amount: stripeRefund.amount / 100,
      currency: stripeRefund.currency,
      status: stripeRefund.status, // succeeded | pending | failed
      stripeRefundId: stripeRefund.id,
      reason: refundRequest.reason,
      notes: refundRequest.notes,
      processedBy: refundRequest.approvedBy, // userId del admin o 'system'
    });

    // 3. Actualizar Payment
    await tx.payment.update({ id: payment.id }, {
      status: 'refunded',
      refundedAt: new Date(),
    });

    // 4. Revocar acceso
    if (payment.type === 'course_purchase') {
      await this.courseService.revokeAccess(payment.userId, payment.courseId);
    }

    return refund;
  });

  // 5. Generar nota de crédito
  await this.invoiceService.generateCreditNote(payment.id);

  // 6. Enviar email
  await this.emailService.sendRefundConfirmation(refund);
}
```

### RF-PAY-006.5: Reembolso Parcial

**DEBE:**
1. Permitir admin especificar monto exacto a reembolsar
2. Validar: `0 < amount <= payment.amount`
3. Actualizar payment parcialmente (no marcar como refunded)
4. Mantener acceso parcial si aplica
5. Registrar monto parcial en `refunds.amount`

**Caso de uso:** Usuario completó 50% del curso, reembolsar 50% del precio.

### RF-PAY-006.6: Reembolso a Wallet vs Método Original

**DEBE:**
1. Por defecto, reembolsar a método de pago original (tarjeta)
2. Si pago fue con wallet, reembolsar a wallet
3. Permitir usuario elegir wallet (si pago fue con tarjeta)
4. Procesar reembolso a wallet instantáneamente
5. Reembolso a tarjeta toma 5-10 días hábiles (limitación bancaria)

### RF-PAY-006.7: Dashboard de Reembolsos (Admin)

**DEBE:**
1. Listar todas las solicitudes pendientes
2. Mostrar información del pago y usuario
3. Mostrar motivo y notas adicionales
4. Permitir aprobar/rechazar con un click
5. Permitir ajustar monto (reembolso parcial)
6. Agregar notas internas

---

## Flujo de Reembolso

### Flujo Automático

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Usuario   │     │  Frontend   │     │   Backend   │     │   Stripe    │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
       │ Click "Solicitar  │                   │                   │
       │ reembolso"        │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │ Selecciona motivo │                   │                   │
       │ + confirma        │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │                   │ POST /refunds     │                   │
       │                   │ { paymentId,      │                   │
       │                   │   reason, notes } │                   │
       │                   │──────────────────▶│                   │
       │                   │                   │                   │
       │                   │                   │ 1. Validate       │
       │                   │                   │    eligibility    │
       │                   │                   │                   │
       │                   │                   │ 2. Check if       │
       │                   │                   │    auto-approve   │
       │                   │                   │    (YES)          │
       │                   │                   │                   │
       │                   │                   │ 3. Create Refund  │
       │                   │                   │──────────────────▶│
       │                   │                   │◀──────────────────│
       │                   │                   │ { refund }        │
       │                   │                   │                   │
       │                   │                   │ 4. Revoke access  │
       │                   │                   │                   │
       │                   │                   │ 5. Update Payment │
       │                   │                   │                   │
       │                   │◀──────────────────│                   │
       │                   │ { status:         │                   │
       │                   │   'approved' }    │                   │
       │                   │                   │                   │
       │◀──────────────────│                   │                   │
       │ "Reembolso        │                   │                   │
       │ procesado"        │                   │                   │
       │                   │                   │                   │
```

### Flujo Manual (Requiere Aprobación)

```
Usuario → Solicita reembolso
   ↓
Backend → Crear RefundRequest (status: pending)
   ↓
Admin → Recibe notificación en dashboard
   ↓
Admin → Revisa motivo y datos del usuario
   ↓
Admin → Decide:
   ├─ Aprobar (full o parcial) → Procesar reembolso
   └─ Rechazar → Enviar email con justificación
```

---

## Modelo de Datos

```typescript
@Entity({ name: 'refund_requests', schema: 'billing' })
class RefundRequest {
  id: string;                    // UUID
  paymentId: string;             // FK a payments
  userId: string;                // FK a users
  amount: Decimal;               // Monto solicitado (null = total)
  reason: RefundReason;
  notes?: string;                // Notas del usuario
  status: RefundRequestStatus;   // pending | approved | rejected | processed
  reviewedBy?: string;           // userId del admin
  reviewNotes?: string;          // Notas del admin
  approvedAt?: Date;
  rejectedAt?: Date;
  processedAt?: Date;
  createdAt: Date;
  updatedAt: Date;
}

@Entity({ name: 'refunds', schema: 'billing' })
class Refund {
  id: string;                    // UUID
  paymentId: string;             // FK a payments
  userId: string;                // FK a users
  amount: Decimal;               // Monto reembolsado
  currency: string;              // USD
  status: RefundStatus;          // succeeded | pending | failed | canceled
  stripeRefundId: string;        // re_xxx
  reason: RefundReason;
  notes?: string;
  processedBy: string;           // 'system' | userId del admin
  destination: RefundDestination; // original_payment_method | wallet
  createdAt: Date;
  updatedAt: Date;
}
```

---

## Reglas de Negocio

### RN-001: Ventanas de Reembolso

| Producto | Condición | Días | Tipo |
|----------|-----------|------|------|
| Curso | Progreso 0% | 7 | Automático |
| Curso | Progreso < 20% | 14 | Manual |
| Curso | Progreso > 20% | 0 | No elegible |
| Suscripción | Cualquiera | 0 | Solo cancelación |
| Wallet topup | Saldo no usado | 30 | Manual |

### RN-002: Límites de Reembolso

- **Máximo 3 reembolsos por usuario por año**
- Usuario con > 3 reembolsos/año → Flagged para revisión
- Usuario con > 5 reembolsos/año → Bloqueado de compras

### RN-003: Fees de Stripe

- Stripe **NO devuelve** el fee de procesamiento (~2.9% + $0.30)
- Trading Platform absorbe este costo
- Calcular pérdida real: `loss = refundAmount * 0.029 + 0.30`

### RN-004: Reembolso de Suscripciones

- Suscripciones **NO son reembolsables**
- Usuario puede cancelar y mantener acceso hasta `currentPeriodEnd`
- Excepción: Error técnico grave → reembolso manual por admin

---

## Seguridad

### Prevención de Abuso

- Trackear ratio de reembolsos por usuario
- Alertar si `refunds / purchases > 50%`
- Bloquear usuarios con patrón abusivo
- Requerir KYC para reembolsos > $100

### Auditoría

- Registrar todos los cambios de estado
- Logs de aprobaciones/rechazos de admin
- Notificar a finanzas de reembolsos > $500

---

## Políticas de Reembolso (Visible al Usuario)

```markdown
# Política de Reembolsos

## Cursos
- **7 días de garantía** si no has iniciado el curso
- **14 días** si has completado menos del 20% del contenido
- Reembolso completo a tu método de pago original

## Suscripciones
- No son reembolsables
- Puedes cancelar en cualquier momento
- Mantienes acceso hasta el final de tu período de facturación

## Procesamiento
- Reembolsos automáticos: Inmediatos (a wallet) o 5-10 días (a tarjeta)
- Reembolsos manuales: 1-3 días hábiles de revisión

## Contacto
Si tienes dudas, contacta a support@trading.com
```

---

## Webhooks Relacionados

| Evento | Acción |
|--------|--------|
| `charge.refunded` | Actualizar Refund a succeeded, enviar email |
| `charge.refund.updated` | Sincronizar estado del reembolso |

---

## Manejo de Errores

| Error | Código | Mensaje Usuario |
|-------|--------|-----------------|
| No elegible | 400 | Este pago no es elegible para reembolso. |
| Ya reembolsado | 409 | Este pago ya fue reembolsado. |
| Fuera de ventana | 400 | El período de reembolso ha expirado. |
| Stripe error | 502 | Error al procesar reembolso. Intenta más tarde. |
| Límite excedido | 403 | Has excedido el límite de reembolsos permitidos. |

---

## Configuración Requerida

```env
# Refund Policy
REFUND_WINDOW_DAYS_ZERO_PROGRESS=7
REFUND_WINDOW_DAYS_LOW_PROGRESS=14
REFUND_PROGRESS_THRESHOLD=20  # %
REFUND_AUTO_APPROVE_AMOUNT=50  # USD
MAX_REFUNDS_PER_YEAR=3

# Admin Notifications
REFUND_APPROVAL_SLACK_WEBHOOK=https://hooks.slack.com/...
```

---

## Métricas de Negocio

- **Refund Rate:** (Reembolsos / Pagos totales) * 100
- **Avg Refund Amount:** Promedio de monto reembolsado
- **Refund Reasons:** Distribución de motivos
- **Time to Refund:** Tiempo promedio de procesamiento
- **Stripe Fee Loss:** Total de fees perdidos por reembolsos

---

## Criterios de Aceptación

- [ ] Usuario puede solicitar reembolso desde historial de pagos
- [ ] Sistema valida elegibilidad según políticas
- [ ] Reembolsos elegibles se aprueban automáticamente
- [ ] Reembolsos complejos van a dashboard de admin
- [ ] Admin puede aprobar/rechazar solicitudes pendientes
- [ ] Admin puede hacer reembolsos parciales
- [ ] Acceso a curso se revoca después de reembolso
- [ ] Reembolso a tarjeta se procesa via Stripe
- [ ] Reembolso a wallet es instantáneo
- [ ] Nota de crédito se genera automáticamente
- [ ] Email de confirmación se envía al usuario
- [ ] Usuario con reembolsos abusivos es flagged

---

## Especificación Técnica Relacionada

- [ET-PAY-006: Refund System](../especificaciones/ET-PAY-006-refunds.md)

## Historias de Usuario Relacionadas

- [US-PAY-010: Ver Historial de Pagos](../historias-usuario/US-PAY-010-ver-historial.md)
