trading-platform/docs/02-definicion-modulos/OQI-005-payments-stripe/requerimientos/RF-PAY-006-reembolsos.md
rckrdmrd c1b5081208 feat(ml): Complete FASE 11 - BTCUSD update and comprehensive documentation alignment
ML Engine Updates:
- Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records
- Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence)
- Backtest results: +176.71R profit with aggressive_filter strategy

Documentation Consolidation:
- Created docs/99-analisis/_MAP.md index with 13 new analysis documents
- Consolidated inventories: removed duplicates from orchestration/inventarios/
- Updated ML_INVENTORY.yml with BTCUSD metrics and training results
- Added execution reports: FASE11-BTCUSD, correction issues, alignment validation

Architecture & Integration:
- Updated all module documentation with NEXUS v3.4 frontmatter
- Fixed _MAP.md indexes across all folders
- Updated orchestration plans and traces

Files: 229 changed, 5064 insertions(+), 1872 deletions(-)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:31:29 -06:00

16 KiB

id title type status priority epic project version created_date updated_date
RF-PAY-006 Sistema de Reembolsos Requirement Done Alta OQI-005 trading-platform 1.0.0 2025-12-05 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


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:

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:

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:

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

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

# 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

# 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

Historias de Usuario Relacionadas