96 lines
5.0 KiB
Markdown
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.
|