trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-006-reembolsos.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

477 lines
16 KiB
Markdown

---
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)
- OrbiQuant 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@orbiquant.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)