Planes personales
Un plan gratuito y planes pagos para cada usuario, con vuelta al gratuito al cancelar o dejar de pagar.
Para un producto donde cada persona paga lo suyo. El ejemplo tiene tres planes: Hobby gratis, Plus y Pro pagos.
El modelo
| En tu aplicación | En Billete |
|---|---|
El usuario user_42 | Un cliente con externalId: "user_42" |
| Su plan | Una sola suscripción, que cambia de plan |
| Hobby | Un plan con precio 0 |
| Plus y Pro | Planes pagos con fallbackPlanId apuntando a Hobby |
- Una suscripción por usuario, para siempre. Subir, bajar, cancelar o dejar de pagar cambian el plan de esa misma suscripción; nunca se crea otra. Su ID no cambia, así que puedes guardarlo en tu usuario.
- Hobby también es una suscripción. Así todo usuario tiene sus cuotas en Billete desde que se registra, y subir de plan es un cambio sobre ella, no una compra aparte.
- El respaldo hace la vuelta a Hobby. Si el usuario cancela o no paga la renovación, Billete pasa la suscripción a Hobby sin que tu aplicación intervenga (vuelta al plan gratuito).
1. Crea los planes
Créalos una vez, desde el dashboard o con el SDK, y guarda sus IDs en la configuración de tu aplicación. Las cuotas (aquí, mensajes) deben existir antes.
const hobby = await billete.plans.create({
name: "Hobby",
lookupKey: "hobby",
currency: "USD",
prices: [{ months: 1, amount: 0 }],
features: { mensajes: 50 },
});
const plus = await billete.plans.create({
name: "Plus",
lookupKey: "plus",
currency: "USD",
prices: [{ months: 1, amount: 1000 }, { months: 12, amount: 10000 }],
gracePeriodDays: 3,
fallbackPlanId: hobby.id,
features: { mensajes: 2000 },
});
// Pro igual que Plus, con otro precio y más cuota.El respaldo tiene que ser gratuito, con puestos incluidos y sin respaldo propio; un plan con respaldo no puede además suspender. Mira puestos y respaldo.
2. Activa Hobby al registrar al usuario
const subscription = await billete.subscriptions.create({
customer: { externalId: user.id, email: user.email },
plan: PLANS.hobby,
months: 1,
}, { idempotencyKey: `hobby-${user.id}` });
await db.users.update(user.id, { subscriptionId: subscription.id });customer como objeto crea el cliente o reutiliza el que ya tiene ese externalId. Un plan gratuito queda active en el acto, sin cobro. La clave de idempotencia hace seguro reintentar; sin ella, una segunda suscripción a Hobby responde subscription_exists.
3. Sube de plan
const result = await billete.subscriptions.changes.create(user.subscriptionId, {
plan: PLANS.pro,
months: 12,
when: "now",
successUrl: "https://tuapp.com/ajustes/plan",
});
if (result.type === "checkout") {
return redirect(result.checkout.url);
}Una subida que cuesta algo devuelve un checkout alojado: el usuario conserva su plan actual hasta que paga, y el cambio se aplica al confirmarse el pago. Entre Plus y Pro, lo que queda del periodo pagado se descuenta del precio nuevo. Para mostrar cuánto pagará antes de enviarlo, usa changes.preview con los mismos datos.
Durante los 3 días previos a la renovación de un plan pago, o en su gracia, un cambio inmediato responde subscription_renewal_open: ofrece pagar la renovación o programar el cambio.
4. Baja de plan o vuelve a Hobby
// De Pro a Plus: conserva Pro hasta el final del periodo que ya pagó.
await billete.subscriptions.changes.create(user.subscriptionId, {
plan: PLANS.plus,
when: "renewal",
});
// A Hobby: cancela al final del periodo y el respaldo hace el resto.
await billete.subscriptions.cancel(user.subscriptionId);Programa las bajadas para la renovación: aplicarlas en el acto pierde lo que queda del periodo pagado, porque no hay saldo a favor (acceptUnusedCredit). Cancelar desde el portal produce lo mismo. Solo si el usuario cierra su cuenta en tu aplicación termina la suscripción del todo, con cancel(id, { atPeriodEnd: false, fallback: false }).
5. Da acceso y escucha los eventos
Lo que el usuario puede usar sale de las cuotas de su plan, así que tu aplicación consulta a Billete en vez de traducir cada plan a permisos propios:
const { allowed } = await billete.features.consume(user.id, "mensajes", 1, {
idempotencyKey: `mensaje-${message.id}`,
});Los eventos sirven para mostrar el plan y avisar al usuario. Las entregas pueden repetirse y llegar en otro orden, así que guarda lo que dice la suscripción al consultarla, no lo que suponga cada evento:
const subscription = await billete.subscriptions.retrieve(event.data.id);
await db.users.updateBySubscription(subscription.id, {
planId: subscription.planId,
status: subscription.status,
});| Evento | Qué pasó | Qué hace tu aplicación |
|---|---|---|
subscription.updated | Cambió el plan: una subida pagada, una cancelación programada o la vuelta a Hobby (previousPlanId dice desde cuál). | Muestra el plan nuevo. Si volvió a Hobby, ofrécele volver a pagar. |
subscription.renewed | Pagó la renovación; si había una bajada programada, ya rige. | Muestra el plan y el periodo nuevos. |
subscription.past_due | Venció sin pago. Conserva el acceso durante la gracia. | Pídele que pague la renovación. |
subscription.change_failed | El pago de una subida llegó tarde y se reembolsa. | Dile que sigue en su plan y que lo intente otra vez. |
En este modelo no llegan subscription.expired ni subscription.canceled: el respaldo los reemplaza por subscription.updated, salvo al cerrar la cuenta con fallback: false.