Files
HoruxDespachos/docs/plans/2026-04-14-reactivate-subscription.md
2026-04-27 22:09:36 -06:00

96 lines
5.0 KiB
Markdown

# 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.