Files
HoruxDespachosNuevo/docs/plans/2026-04-14-reactivate-subscription.md

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.tscreatePreapproval ahora acepta startDate?: Date opcional (traduce a auto_recurring.start_date ISO si es fecha futura; MP rechaza fechas pasadas)
  • apps/api/src/services/payment/subscription.service.tsreactivateSubscription({ tenantId, payerEmail })
  • apps/api/src/controllers/subscription.controller.tsreactivateMe handler
  • apps/api/src/routes/subscription.routes.tsPOST /api/subscriptions/me/reactivate

Frontend

  • apps/web/lib/api/subscription.tsreactivateMe()
  • apps/web/lib/hooks/use-subscription.tsuseReactivateMe()
  • apps/web/app/(dashboard)/configuracion/suscripcion/page.tsx:
    • Handler handleReactivate — llama mutation, abre paymentUrl en nueva pestaña
    • Banner naranja isCancelledInPeriod incluye 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

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

  1. Email confirmación de reactivación — análogo al de cancelación, para cerrar el loop de comunicación.
  2. 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.