BLOCKER-001: Token Refresh Improvements (FASE 4 frontend) - Proactive refresh scheduler: refresh 5min before token expiry - Multi-tab synchronization with BroadcastChannel API - Automatic scheduling on X-Token-Expires-At header reception - Background token refresh to prevent user interruption E2E Tests: Video Upload Module (frontend - 62 tests) - Suite 1: Form tests (27 tests) - 3-step wizard validation - Suite 2: Service tests (20 tests) - Multipart upload logic - Suite 3: Integration tests (15 tests) - Complete flow validation Test infrastructure: - vitest.config.ts (NEW) - Vitest configuration with jsdom - src/__tests__/setup.ts (NEW) - Global test setup and mocks - Updated payments-stripe-elements.test.tsx to use Vitest (vi.mock) Changes: - apiClient.ts: Proactive refresh scheduler + BroadcastChannel sync - payments-stripe-elements.test.tsx: Migrated from Jest to Vitest Tests created: - video-upload-form.test.tsx (27 tests) - Component validation - video-upload-service.test.ts (20 tests) - Service logic validation - video-upload-integration.test.tsx (15 tests) - Integration flow Additional documentation: - Module README.md files for assistant, auth, education, investment, payments, portfolio, trading - Investment module: Analysis, contracts, gaps, delivery documentation - Payments module: Stripe integration, wallet specification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
18 KiB
OQI-005: Análisis de Gaps - Funcionalidades Faltantes
Módulo: OQI-005 (pagos-stripe) Fecha: 2026-01-25 Criticidad: Las siguientes features no están implementadas pero serían valiosas para UX completa
Gap 1: Refunds UI (Devoluciones)
Prioridad: Alta Complejidad: Media Impacto: UX, Legal, Soporte
Descripción del Gap
Actualmente no existe:
- UI para iniciar devoluciones de pagos
- Panel de historial de reembolsos procesados
- Validación de período de devolución (ej: 30 días)
- Estados de devolución (pending, processing, completed, failed)
- Notificaciones al usuario cuando devolución se procesa
Casos de Uso
1. Usuario solicita reembolso de suscripción
├── Verifica si está dentro de período permitido (14 días)
├── Selecciona razón de devolución
├── Backend procesa via Stripe API
├── Estado se actualiza a "pending"
└── Usuario notificado cuando se completa (1-3 días)
2. Usuario ve historial de reembolsos
├── Tabla en Billing.tsx → Nueva tab "Reembolsos"
├── Muestra:
│ ├── Fecha de solicitud
│ ├── Monto reembolsado
│ ├── Motivo
│ ├── Estado (pending/completed/failed)
│ └── Método devolución (mismo método pago original)
└── Filtros: status, fecha range
3. Administrador revisa reembolso rechazado
├── Dashboard de admin ver motivo rechazo
├── Opción de re-procesar
└── Notificar usuario
Componentes Necesarios
// 1. RefundRequestModal (modal para solicitar devolución)
interface RefundRequestModalProps {
isOpen: boolean;
onClose: () => void;
subscriptionId: string;
daysRemaining: number; // Días dentro período permitido
refundableAmount: number;
onSuccess?: () => void;
}
// 2. RefundList (tabla historial reembolsos)
interface RefundListProps {
subscriptionId?: string;
compact?: boolean;
itemsPerPage?: number;
}
// 3. RefundDetail (modal detalle reembolso)
interface RefundDetailProps {
refundId: string;
onClose: () => void;
}
Endpoint Backend Requerido
POST /payments/refunds
├── Body: { subscriptionId, amount, reason, requestedAt }
├── Valida: Dentro de período permitido (30 días default)
├── Respuesta: { id, status, amount, reason, processedAt? }
└── Webhook: refund.completed → Actualizar wallet usuario
GET /payments/refunds
├── Query params: subscriptionId, status, dateRange
└── Respuesta: Paginated list
GET /payments/refunds/{id}
├── Detalle de reembolso
└── Historial de actualizaciones
DELETE /payments/refunds/{id}
├── Solo si status=pending
└── Cancelar devolución pendiente
Lógica de Negocio
Período Devolución: 14 días desde primera facturación
├── Free trial: +7 días adicionales
├── Payment failed: No aplica devolución
└── Upgrade/downgrade: Proporcional a días usados
Estados:
├── pending: Enviada a Stripe, sin procesar
├── processing: Stripe la está procesando
├── completed: Completada, fondos devueltos
├── failed: Rechazada por banco/Stripe
└── cancelled: Usuario canceló solicitud
Reglas:
├── 1 reembolso/cliente/30 días (máximo)
├── Monto: hasta 100% de suscripción
└── Notificación: Email cuando se complete
Estimación Esfuerzo
- Backend: 8-12 horas (Stripe API integration, webhook, DB)
- Frontend UI: 6-8 horas (modal, tabla, detalle)
- Testing: 4-6 horas
- Documentación: 2-3 horas
- Total: ~20-29 horas
Gap 2: Histórico de Cambios de Plan
Prioridad: Media Complejidad: Baja Impacto: UX, Auditoría, Soporte
Descripción del Gap
Actualmente no existe:
- Visualización del historial de cambios de plan
- Fechas de cambio y razones
- Comparativa plan anterior vs nuevo
- Créditos/cargos aplicados en cada cambio
- Timeline visual de evolución de suscripción
Casos de Uso
1. Usuario revisa su historial de planes
├── Nueva tab en Billing: "Historial de Planes"
├── Timeline/tabla mostrando:
│ ├── Fecha cambio
│ ├── Plan anterior → Plan nuevo
│ ├── Precio anterior → Precio nuevo
│ ├── Crédito prorrateo (si aplica)
│ ├── Cargo adicional (si aplica)
│ └── Razón del cambio (upgrade/downgrade/etc)
└── Botones: Ver detalles, Descargar recibo
2. Usuario entiende su evolución de suscripción
├── Cuándo cambió de Free → Pro
├── Cuándo hizo upgrade Pro → Enterprise
└── Visualizar inversión total en plataforma
3. Soporte consulta historial cliente
├── Dashboard admin con historia completa
├── Auditoría de cambios (quién, cuándo)
└── Facilita resolución de disputas
Componentes Necesarios
// 1. PlanHistoryTimeline (visualización timeline)
interface PlanHistoryTimelineProps {
subscriptionId: string;
compact?: boolean;
}
// 2. PlanChangeDetail (modal detalle de cambio)
interface PlanChangeDetailProps {
changeId: string;
onClose: () => void;
}
Estructura de Datos
interface SubscriptionPlanChange {
id: string;
subscriptionId: string;
planIdFrom: string;
planNameFrom: string;
planIdTo: string;
planNameTo: string;
changeType: 'upgrade' | 'downgrade' | 'lateral_change';
billingCycleFrom: 'monthly' | 'yearly';
billingCycleTo: 'monthly' | 'yearly';
amountFrom: number;
amountTo: number;
proratedCredit: number;
chargeAmount: number;
netAmount: number;
effectiveDate: string;
reason?: string;
initiatedBy: 'user' | 'admin' | 'system';
invoiceId?: string;
createdAt: string;
}
Endpoint Backend Requerido
GET /payments/subscription/changes
├── Query params: subscriptionId, limit, offset
├── Respuesta: Paginated list of changes
└── Incluye: amountBefore, amountAfter, credits, invoiceId
GET /payments/subscription/changes/{id}
├── Detalle cambio
├── Incluye: invoiceDetails si aplica
└── Historial de estados (si cambio fue procesado en fases)
Lógica de Negocio
Cambio de Plan:
├── Fecha: Inmediata (upgrade) o fin de período (downgrade)
├── Crédito: Prorrateo por días no usados del plan anterior
├── Cargo: Diferencia (si upgrade) o crédito (si downgrade)
├── Factura: Nueva línea en siguiente ciclo o invoice immediata
└── Notificación: Confirmación al usuario
Datos Calculados:
├── changeType: Compara precio plan anterior vs nuevo
├── Prorration: (daysRemaining / daysInPeriod) * planPrice
└── Timeline: Mostrar cuando cambios se aplicaron
Estimación Esfuerzo
- Backend: 4-6 horas (queries, DB si no existe tabla)
- Frontend UI: 4-6 horas (timeline, tabla, modal)
- Testing: 2-3 horas
- Documentación: 1-2 horas
- Total: ~11-17 horas
Gap 3: Preview de Factura
Prioridad: Media Complejidad: Baja Impacto: UX, Confianza del Usuario
Descripción del Gap
Actualmente no existe:
- Preview de factura ANTES de completar pago
- Visualización de items que se facturarán
- Cálculo claro de subtotal, impuestos, descuentos
- Confirmación de direcciones de facturación
- Mostrar si hay cambios desde checkout anterior
Casos de Uso
1. Usuario en checkout ve qué va a pagar
├── Antes de hacer click "Pagar"
├── Muestra:
│ ├── Plan seleccionado
│ ├── Precio
│ ├── Período (1 mes, 1 año)
│ ├── Subtotal
│ ├── Impuestos calculados (si aplica)
│ ├── Descuentos (cupones)
│ ├── Total a pagar
│ ├── Ciclo de facturación
│ ├── Dirección de facturación
│ └── Método de pago (últimos 4 dígitos)
└── Botón: "Confirmar y Pagar"
2. Usuario cambia información de facturación
├── Actualiza dirección
├── Sistema recalcula impuestos
├── Preview se actualiza automáticamente
└── Muestra cambio en total (si aplica)
3. Usuario aplica cupón
├── Ingresa código
├── Preview actualiza con nuevo total
├── Muestra descuento claramente
└── Botón confirmar disponible
Componentes Necesarios
// 1. InvoicePreview (modal/drawer con preview factura)
interface InvoicePreviewProps {
planId: string;
billingCycle: 'monthly' | 'yearly';
couponCode?: string;
billingInfo?: BillingInfo;
paymentMethodId?: string;
onConfirm?: () => void;
onCancel?: () => void;
}
// Integrado en CheckoutFlow (modal/step adicional)
Estructura de Datos
interface InvoicePreview {
planId: string;
planName: string;
description: string;
subtotal: number;
tax: number;
taxRate?: number;
discount: number;
discountReason?: string;
total: number;
currency: string;
billingCycle: 'monthly' | 'yearly';
itemizedBreakdown: {
description: string;
quantity: number;
unitPrice: number;
amount: number;
}[];
billingInfo: {
name: string;
email: string;
address: string;
};
paymentMethod: {
type: string;
last4: string;
brand: string;
};
nextBillingDate: string;
estimatedTotal12Months?: number; // Si yearly, show equiv annual
}
Endpoint Backend Requerido
POST /payments/invoice/preview
├── Body: {
│ planId,
│ billingCycle,
│ couponCode?,
│ billingInfo?,
│ paymentMethodId?
├── Valida: País (para impuestos), cupón (si existe)
├── Calcula: Impuestos según dirección billing
└── Respuesta: InvoicePreview completo
GET /payments/taxes/estimate
├── Query: country, state, zipcode
├── Respuesta: { taxRate, taxName }
└── Usado para cálculos en tiempo real
Lógica de Negocio
Cálculo de Impuestos:
├── Por país/estado
├── VAT en EU (21% típicamente)
├── Sales tax en USA (5-10% según estado)
├── GST en CA (5%)
├── Integración con TaxJar/Avalara (opcional)
└── Almacenar en factura final
Créditos (si aplica):
├── Trial period: Mostrar cuando se cobra
├── Prorated: Crédito plan anterior (si existe)
├── Coupon: Descuento aplicado
└── One-time: Bonificación admin
Preview Real-time:
├── Usuario actualiza dirección → Recalcula impuestos
├── Usuario escribe cupón → Valida y recalcula
├── Usuario cambia billing cycle → Recalcula todo
└── Debounce requests para no sobrecargar backend
Estimación Esfuerzo
- Backend: 6-8 horas (cálculo impuestos, endpoints)
- Frontend UI: 4-6 horas (modal, forms, updates)
- Tax Integration: 2-4 horas (si usa servicio externo)
- Testing: 3-4 horas
- Documentación: 1-2 horas
- Total: ~16-24 horas
Gap 4: Payment Intent Flow (Completo)
Prioridad: Alta (Seguridad) Complejidad: Alta Impacto: Seguridad, PCI-DSS Compliance
Descripción del Gap
Actualmente:
- ✅ Usa Stripe Hosted Checkout (seguro, delegado)
- ❌ NO implementa Payment Intent flow en cliente
- ❌ Métodos de pago sin tokenización Stripe.js
- ❌ NO soporta 3D Secure / SCA en cliente (delegado a backend)
- ❌ NO maneja tarjetas declinadas gracefully
Casos de Uso
1. Usuario completa checkout con tarjeta nueva (Hosted Checkout)
├── Redirección a Stripe
├── Stripe maneja validación, 3DS, etc
├── Redirige back a CheckoutSuccess
└── ✅ Funciona (implementado)
2. Usuario agrega método de pago desde Billing
├── Abre formulario PaymentMethodForm
├── ❌ Envía número de tarjeta SIN tokenizar
├── Backend debería rechazar (PCI risk)
└── Debería usar Stripe.js Elements
3. Usuario paga con saved payment method
├── No aplica (solo checkout con hosted)
└── Si se implementa: requiere Payment Intent
4. 3D Secure required
├── Backend detecta SCA required
├── Devuelve clientSecret
├── ❌ Frontend no maneja confirmación
├── Debería usar Stripe.js confirmCardPayment
└── Mostrar modal de autenticación
Componentes Necesarios
// 1. StripeProvider (wrappear app)
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/js';
const stripePromise = loadStripe(STRIPE_PUBLIC_KEY);
// 2. PaymentMethodForm refactorizado
interface PaymentMethodFormProps {
onSuccess: (paymentMethodId: string) => void;
onCancel?: () => void;
onError?: (error: string) => void;
setAsDefault?: boolean;
}
// 3. PaymentIntentModal (para confirmar 3DS)
interface PaymentIntentModalProps {
clientSecret: string;
onSuccess: () => void;
onError: (error: string) => void;
}
Cambios Necesarios
// BEFORE (Inseguro)
const handleSubmit = async (e) => {
const result = await addPaymentMethod({
type: 'card',
card: {
number: cardNumber, // ❌ PCI risk
exp_month: expiry.split('/')[0],
exp_year: 2000 + parseInt(expiry.split('/')[1]),
cvc: cvc, // ❌ PCI risk
},
});
};
// AFTER (Seguro con Stripe.js)
const { stripe, elements } = useStripe();
const handleSubmit = async (e) => {
const cardElement = elements.getElement(CardElement);
// Crear payment method en Stripe (tokenizado)
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (!error) {
// Enviar token, no tarjeta
const result = await addPaymentMethod(paymentMethod.id);
}
};
Endpoint Backend Requerido
POST /payments/methods
├── Body: { paymentMethodId, setAsDefault }
├── paymentMethodId: Token Stripe (ej: pm_1234567890)
├── Almacena en Stripe + DB
└── ✅ Compliant PCI-DSS
POST /payments/subscriptions/{id}/confirm-payment
├── Body: { clientSecret, paymentMethodId }
├── Si requiere SCA: Procesa autenticación
├── Respuesta: { success, paymentStatus }
└── Usado para 3D Secure flow
Estimación Esfuerzo
- Backend: 4-6 horas (validar tokens, SCA handling)
- Frontend Stripe.js: 8-10 horas (Elements, intent confirmation)
- Testing (incluye SCA test): 6-8 horas
- Documentación: 2-3 horas
- Total: ~20-27 horas
Gap 5: Apple Pay / Google Pay
Prioridad: Baja Complejidad: Media Impacto: UX, Conversión (móvil)
Descripción del Gap
Actualmente no existe soporte para:
- Apple Pay (en iOS/macOS)
- Google Pay (en Android/Chrome)
- Botones "Pay with X" en checkout
Estimación Esfuerzo
- Backend: 2-4 horas (endpoint validation)
- Frontend: 6-8 horas (Stripe Payment Request Button)
- Testing: 4-6 horas (necesita dispositivos)
- Total: ~12-18 horas
Gap 6: Retry Logic para Pagos Fallidos
Prioridad: Media Complejidad: Media Impacto: Retención, Ingresos
Descripción del Gap
Actualmente:
- ❌ No hay reintentos automáticos de pagos fallidos
- ❌ No hay notificación al usuario cuando falla
- ❌ No hay UI para re-procesar pago manual
- ❌ No hay webhook handling para payment_intent.payment_failed
Casos de Uso
1. Suscripción tiene status "past_due"
├── Payment falló (tarjeta rechazada, fondos insuficientes)
├── Sistema envía email al usuario
├── Usuario puede ver deuda pendiente en Billing
├── Usuario puede:
│ ├── Actualizar método de pago
│ └── Click "Reintentar pago"
└── Stripe reintenta automáticamente (3 veces en 3 días)
2. Dashboard muestra estado "Pago Pendiente"
├── Badge rojo en SubscriptionCard
├── Info: "Tenemos un problema con tu pago"
├── Botones:
│ ├── Actualizar método de pago
│ └── Reintentar ahora
└── Link a portal Stripe
Endpoints Requeridos
POST /payments/subscription/{id}/retry-payment
├── Reintenta el pago pendiente
├── Requiere valid payment method
└── Respuesta: { success, newStatus, nextRetryDate? }
GET /payments/failed-payments
├── Listar pagos fallidos del usuario
└── Respuesta: Array de intentos fallidos con razones
Estimación Esfuerzo
- Backend: 4-6 horas (retry logic, webhooks)
- Frontend: 3-4 horas (UI, forms)
- Testing: 2-3 horas
- Total: ~9-13 horas
Priorización Recomendada
Fase 1 (MVP - 2-3 semanas)
- Preview de Factura (16-24 hrs) - Mejor UX antes de pagar
- Retry Logic (9-13 hrs) - Aumenta retención
Fase 2 (3-4 semanas)
- Refunds UI (20-29 hrs) - Cumplimiento legal, UX completa
- Histórico Cambios Plan (11-17 hrs) - Soporte + auditoría
Fase 3 (2-3 semanas)
- Payment Intent Flow (20-27 hrs) - Seguridad PCI-DSS
- Apple Pay / Google Pay (12-18 hrs) - Conversión móvil
Impacto de No Implementar
| Gap | Riesgo | Impacto |
|---|---|---|
| Refunds | Legal/Cumplimiento | Posibles multas/disputas |
| Historial Cambios | Soporte | Tickets más complejos |
| Preview Factura | UX | Menor confianza en checkout |
| Payment Intent | Seguridad | PCI-DSS non-compliant |
| Apple/Google Pay | Conversión | Pérdida clientes móviles |
| Retry Logic | Ingresos | Pérdida 5-10% suscriptores |
Checklist de Implementación
Cuando se implementen estos gaps:
- Crear rama feature separada por gap
- Actualizar tipos TypeScript en
payment.types.ts - Documentar endpoints en
OQI-005-CONTRATOS-API.md - Agregar tests unitarios + integración
- Actualizar
paymentStore.tscon nuevos métodos - Crear nuevos componentes en
components/payments/ - Integrar en
Billing.tsxcon nuevas tabs - Validar con Stripe test keys
- Documentar en nuevos archivos OQI-005-*.md
- Rebasar a main una vez QA aprobado
- Desplegar a producción con feature flags