# 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` — `createPreapproval` 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.ts` — `reactivateSubscription({ tenantId, payerEmail })` - `apps/api/src/controllers/subscription.controller.ts` — `reactivateMe` handler - `apps/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, 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.