5.0 KiB
Reactivar suscripción cancelada dentro del período pagado
Resumen
Cuando un cliente cancela su suscripción, el acceso continúa hasta currentPeriodEnd (política existente). Antes, durante esa ventana no había manera de revertir la cancelación — el cliente tenía que esperar a que expirara para re-contratar. Ahora puede reactivarla con un botón desde /configuracion/suscripcion.
Motivación
UX real: cliente cancela por error o cambia de opinión, quiere volver. Forzar que espere a fin de período + re-contratar desde cero es fricción innecesaria (y posible pérdida si se van con la competencia en esa ventana).
Mecánica MercadoPago
Un preapproval cancelado en MP es terminal — no se puede reactivar. Reactivar = crear un preapproval nuevo con los mismos parámetros (plan, amount, frequency) y auto_recurring.start_date apuntando al final del período actual para evitar doble cobro.
T0 (hoy): cancelled, currentPeriodEnd = T0+15d
T0: Usuario click "Reactivar"
→ backend crea NUEVO preapproval
reason: "Reactivación Plan X (frequency) - Tenant"
amount: mismo
frequency: mismo
start_date: T0+15d ← clave para no cobrar doble
→ Subscription.status = 'pending'
→ Subscription.mpPreapprovalId = nuevo ID
→ Retorna paymentUrl
T0..T0+14d: Usuario sigue teniendo acceso (período ya pagado)
T0+15d: MP ejecuta primer cobro → webhook → status 'authorized'
T0+15d+30d (o +365d): siguiente cobro
Reglas y validaciones
| Condición | Resultado |
|---|---|
| Tenant no tiene suscripción cancelada | 400 "No hay suscripción cancelada para reactivar" |
Suscripción cancelled pero currentPeriodEnd ya venció |
400 "El período pagado ya venció — contrata un nuevo plan desde el selector" (UI ya lo sabe: isCancelledExpired muestra el picker) |
| Plan cancelado era custom | 400 "Reactivación de plan custom requiere coordinación con el admin global" |
| MP no configurado | 503 con mensaje claro |
| MP rechaza (token inválido) | 503 "MercadoPago rechazó la solicitud" |
| Reactivación exitosa | Limpia pendingPlan, upgradeTargetPlan, etc. El estado queda limpio como una sub recién contratada |
Archivos
Backend
apps/api/src/services/payment/mercadopago.service.ts—createPreapprovalahora aceptastartDate?: Dateopcional (traduce aauto_recurring.start_dateISO si es fecha futura; MP rechaza fechas pasadas)apps/api/src/services/payment/subscription.service.ts—reactivateSubscription({ tenantId, payerEmail })apps/api/src/controllers/subscription.controller.ts—reactivateMehandlerapps/api/src/routes/subscription.routes.ts—POST /api/subscriptions/me/reactivate
Frontend
apps/web/lib/api/subscription.ts—reactivateMe()apps/web/lib/hooks/use-subscription.ts—useReactivateMe()apps/web/app/(dashboard)/configuracion/suscripcion/page.tsx:- Handler
handleReactivate— llama mutation, abrepaymentUrlen nueva pestaña - Banner naranja
isCancelledInPeriodincluye botón "Reactivar suscripción" - Card "Tu Suscripción" muestra "Reactivar" como acción principal (primary) en vez de "Cancelar"
- Limpia la condición redundante
(isActive || ... || isCancelledInPeriod) && !isCancelledInPeriod
- Handler
Flow UI
Estado: isCancelledInPeriod (status='cancelled', currentPeriodEnd en el futuro)
↓
Banner naranja "Suscripción cancelada — tienes acceso hasta X"
+ botón inline "Reactivar suscripción"
↓
Card "Tu Suscripción" con botón primary "Reactivar suscripción"
↓
Click → POST /me/reactivate → { paymentUrl }
↓
window.open(paymentUrl, '_blank') → MP checkout
↓
Usuario autoriza en MP
↓
Webhook `subscription_preapproval` → status='authorized'
↓
UI refresca (React Query invalidation) → muestra "Activa"
Decisiones descartadas
Reactivar con cambio de plan en el mismo flow
Tentación: mostrar modal con picker en vez de botón "Reactivar" plano.
Por qué no: complica el flujo (reactivar + cambiar plan = dos semánticas mezcladas). MVP = simple: reactiva con mismo plan. Si quiere otro plan, usa "Cambiar plan" después de reactivar (ya existe).
"Undo cancel" retentivo (sin re-autorización de MP)
Tentación: si la cancelación fue hace < 24h y el preapproval MP aún está paused (no cancelled), teóricamente podríamos re-activarlo sin crear uno nuevo.
Por qué no: cancelSubscription llama cancelPreapproval inmediatamente → MP lo marca cancelled terminal. Recuperarlo requeriría: (a) no cancelar el preapproval al cancelar, (b) cron que lo cancele 7 días después. Complejidad innecesaria para un caso marginal.
Pendientes
- Email confirmación de reactivación — análogo al de cancelación, para cerrar el loop de comunicación.
- Métrica de reactivaciones — útil para el admin global: cuántos clientes cancelan pero reactivan antes del fin de período. Indicador de UX y de churn real.