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