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>
16 KiB
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:
- Permitir solicitar reembolso desde historial de pagos
- Mostrar elegibilidad según políticas
- Solicitar motivo (lista predefinida + campo libre)
- Confirmar acción con advertencia de consecuencias
- 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:
- Pago existe y status =
succeeded - No se ha reembolsado previamente
- Dentro de ventana de reembolso permitida
- Usuario es el propietario del pago
- 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:
- Crear registro en
billing.refunds - Llamar Stripe API:
stripe.refunds.create() - Revocar acceso al recurso (curso, suscripción)
- Actualizar
payment.status = 'refunded' - Generar nota de crédito (factura)
- 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:
- Permitir admin especificar monto exacto a reembolsar
- Validar:
0 < amount <= payment.amount - Actualizar payment parcialmente (no marcar como refunded)
- Mantener acceso parcial si aplica
- 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:
- Por defecto, reembolsar a método de pago original (tarjeta)
- Si pago fue con wallet, reembolsar a wallet
- Permitir usuario elegir wallet (si pago fue con tarjeta)
- Procesar reembolso a wallet instantáneamente
- Reembolso a tarjeta toma 5-10 días hábiles (limitación bancaria)
RF-PAY-006.7: Dashboard de Reembolsos (Admin)
DEBE:
- Listar todas las solicitudes pendientes
- Mostrar información del pago y usuario
- Mostrar motivo y notas adicionales
- Permitir aprobar/rechazar con un click
- Permitir ajustar monto (reembolso parcial)
- 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