Plans y billing (Stripe)
CÉNIT tiene tres planes — esencial, pro y enterprise — más un
estado expired. La definición vive en lib/plans.ts (23 features +
límites por recurso). El webhook de Stripe está operativo y es
idempotente. La página /upgrade existe y muestra los planes,
pero el handler GET /api/stripe/checkout que invoca el botón no
está implementado (el directorio app/api/stripe/checkout/ no
existe — solo app/api/stripe/webhook/). Activar el flow self-service
requiere implementar el handler y configurar
STRIPE_PRICE_ID_PRO / STRIPE_PRICE_ID_ENTERPRISE en Workers
Secrets. Hay además gaps reales entre lo que se vende y lo que está
construido — documentados acá honestamente.
Para comercial
Sección titulada «Para comercial»Qué ofrece cada plan
Sección titulada «Qué ofrece cada plan»| Plan | Atletas | Equipos | Colaboradores | Historial |
|---|---|---|---|---|
esencial | 40 | 2 | 5 | 24 meses |
pro | ilimitado | ilimitado | ilimitado | ilimitado |
enterprise | ilimitado | ilimitado | ilimitado | ilimitado |
expired | 0 | 0 | 0 | 0 |
Gaps conocidos — leer antes de vender
Sección titulada «Gaps conocidos — leer antes de vender»7 features prometidas pero NO implementadas (solo en pro/enterprise):
custom_dashboard, excel_export, ai_post_injury_analysis,
ai_anomaly_detection, ai_natural_language_insights,
wearables_integration, api_read. Vender Pro/Enterprise hoy
prometiendo estas implica deuda con el cliente.
Pro y Enterprise son idénticos en código — lib/plans.ts define
exactamente las mismas 23 features en true para ambos planes. No
hay diferenciador funcional. Si Enterprise debe tener algo más (SSO,
audit logs, white-label fase 2, soporte dedicado) requiere decisión
de producto.
2 features sin gate (esencial las puede usar gratis):
season_comparisons y custom_branding. Está identificado y queda
como deuda técnica corta.
Cómo lo usa el staff
Sección titulada «Cómo lo usa el staff»Gating en código
Sección titulada «Gating en código»import { canUse } from '@/lib/plans'if (!canUse(plan, 'risk_advisor')) return nullEl plan vive en user_profiles.plan. Si es expired, el layout del
dashboard redirige a /upgrade?expired=true.
Flujo de pago (parcialmente implementado)
Sección titulada «Flujo de pago (parcialmente implementado)»/upgrademuestra los tres planes con precios — listo.- El botón “Elegir plan” linkea a
/api/stripe/checkout?plan=<key>— handler NO existe, hoy devuelve 404. - Cuando el handler exista deberá crear la sesión de Stripe Checkout
con
metadata.plany redirigir al checkout hosted. - Stripe redirige a
/upgrade/successo/upgrade/cancel. - Stripe dispara webhook a
/api/stripe/webhook→ el plan se actualiza enuser_profiles. Esto sí está operativo.
Datos y métricas
Sección titulada «Datos y métricas»Webhook (operativo)
Sección titulada «Webhook (operativo)»POST /api/stripe/webhook (app/api/stripe/webhook/route.ts)
verifica firma con STRIPE_WEBHOOK_SECRET y procesa cuatro eventos:
checkout.session.completed→ actualizaplan,stripe_customer_id,stripe_subscription_id. Devuelve 404 explícito si el email no matchea ningúnuser_profiles(antes silencioso — ahora visible en el dashboard de Stripe).customer.subscription.updated→ guardaplan_expires_at.customer.subscription.deleted→ seteaplan = 'expired'.invoice.payment_failed→ seteaplan = 'expired'. El layout redirige al usuario a/upgradede inmediato.
Idempotencia
Sección titulada «Idempotencia»Cada event.id se claimea en stripe_processed_events (PK
event_id). Si Stripe reintenta, el segundo POST retorna
{ received: true, idempotent: true } sin re-mutar. La unique
violation 23505 es la garantía atómica.
Tablas DB
Sección titulada «Tablas DB»user_profiles—plan,stripe_customer_id,stripe_subscription_id,plan_expires_at.stripe_processed_events— lock de idempotencia.
Integraciones
Sección titulada «Integraciones»- Sentry — todo error del webhook se
captura con tags
route: stripe/webhook+event+eventId. - Multi-tenant — el plan es
por-usuario (en
user_profiles), no por-organización.
Limitaciones / roadmap
Sección titulada «Limitaciones / roadmap»- Checkout self-service incompleto —
app/upgrade/page.tsxexiste en main, pero el handlerapp/api/stripe/checkout/route.tsque el botón invoca no está creado. Falta también configurarSTRIPE_PRICE_ID_PROySTRIPE_PRICE_ID_ENTERPRISEen Workers Secrets. - 7 features inexistentes — decisión de producto: construir, sacar de la oferta o documentar como roadmap H2 2026.
- Diferenciador Pro/Enterprise — pendiente.
- Gating de
season_comparisons+custom_branding— ~2-3 h de trabajo, pendiente. - Bug menor:
dashboard/medico/actions.tsgatea conmodule_physio(debería sermodule_medical, que no existe en la lista de features).