Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

Activa los planes y el cobro

Las llaves, banderas y el webhook que hacen existir las suscripciones, y qué aparece cuando existen.

El cobro viene apagado en una instalación nueva. Esta guía cubre todo lo que configuras antes de poder vender un plan: las llaves de Stripe, las banderas que hacen existir las pantallas, las variables de Cashier, el webhook y cómo probarlo. Crear los planes en sí está en Crea y pon precio a un plan.

Sobre qué corre

Las suscripciones son Laravel Cashier 15 sobre Stripe. Stripe es la fuente de verdad: las tablas locales reflejan lo que Stripe reporta por el webhook, y nada se cobra ni se suscribe fuera de Stripe. El código vive en el paquete local packages/billing-core (weblabormx/billing-core), que la aplicación requiere por ruta.

El cliente de Cashier no es el usuario. Cada usuario posee una fila BillingAccount (tabla billing_accounts), y esa cuenta guarda el id de cliente de Stripe, el resumen del método de pago y las suscripciones. App\Models\User llega a ella por el trait App\Traits\HasPlans, así que en tu código sigues escribiendo $user->subscription(), $user->currentPlan() o $user->reachedLimit('key'); el trait reenvía a la cuenta. El trait no tiene dependencia compilada con el paquete, y por eso la aplicación arranca con el cobro removido.

Los interruptores

Nada relacionado con cobro aparece hasta que esto está puesto. .env.example lista las llaves de Stripe y las dos banderas; las variables BILLING_* no están en él porque su valor por omisión ya es encendido.

Variable Valor por omisión Qué controla
STRIPE_KEY vacío Llave publicable. La carga Stripe.js en la pantalla de métodos de pago.
STRIPE_SECRET vacío Llave secreta. Toda llamada a Stripe, y una condición para que los recursos Planes y Complementos del panel de administración existan siquiera.
STRIPE_WEBHOOK_SECRET vacío Secreto de firma del endpoint del webhook. Las peticiones sin una firma válida se rechazan.
FEATURE_PLANS_ENABLED false Planes: el recurso del panel, /account/plans, /account/billing, /account/payment-methods, el middleware de suscripción, la columna y el filtro de plan en la lista de usuarios.
FEATURE_ADDONS_ENABLED false Complementos: el recurso del panel, /account/add-ons, el panel de complementos en el formulario de usuario. Ver Vende complementos.
BILLING_CORE_ENABLED true El paquete completo. En false el proveedor no registra nada: ni migraciones, rutas, comandos, políticas, observadores ni configuración de Cashier.
BILLING_USER_ENABLED true Sólo el adaptador de usuario: las rutas /account/* de cobro, el listener que suscribe a un usuario nuevo al plan gratuito, y el alias del middleware ensure.subscribed. Las tablas compartidas y los recursos del panel siguen disponibles para otro adaptador.

Las dos banderas se leen de config/features.php (plans_enabled, addons_enabled); las dos variables BILLING_* de config/billing-core.php (enabled, user_adapter.enabled).

Con FEATURE_PLANS_ENABLED y FEATURE_ADDONS_ENABLED apagadas, el panel de administración no muestra ningún grupo Planes: ni Planes, ni Complementos. Es el error de despliegue más común: las llaves están puestas, la migración corrió, y la barra lateral sigue sin nada. Pasa lo mismo con la bandera encendida y STRIPE_SECRET vacío, porque las políticas de plan y complemento rechazan toda acción cuando falta el secreto. Pon la bandera y el secreto, limpia la caché de configuración, y el grupo aparece para quien tenga el permiso retrieve billing_plan.

Los planes se consideran vivos para los usuarios sólo cuando tres cosas son ciertas a la vez: la bandera está encendida, STRIPE_SECRET está puesto, y existe al menos un plan. Eso es lo que revisa BillingPlan::isActive(), y es la condición detrás de cada pantalla del usuario. Hasta que creas el primer plan, el menú de la cuenta no muestra nada y el middleware de suscripción deja pasar a todos.

Las variables de Cashier

Cashier lee su propia configuración de vendor/laravel/cashier/config/cashier.php, que el kit no publica. Estas son las variables que lee y qué hace cada una en esta aplicación.

Variable Valor por omisión Qué hace aquí
CASHIER_CURRENCY usd Moneda de respaldo de Cashier: la que usa cuando un monto se formatea o se cobra sin moneda propia. Los precios de suscripción no la usan, porque cada Stripe Price trae su propia moneda. Los cobros únicos sí; ver Cobra una vez con Stripe Checkout.
CASHIER_CURRENCY_LOCALE en Configuración regional para formatear dinero, como los totales de facturas en /account/billing. Cualquier valor distinto de en necesita la extensión intl de PHP.
CASHIER_PATH stripe Prefijo de las rutas que Cashier registra por su cuenta: /stripe/payment/{id} (la página donde un cliente confirma un pago que requiere autenticación) y /stripe/webhook.
CASHIER_PAYMENT_NOTIFICATION vacío Clase de notificación que se envía cuando Stripe reporta invoice.payment_action_required. Ponle Laravel\Cashier\Notifications\ConfirmPayment para enviar al cliente un correo con el enlace a la página de confirmación. Vacío significa sin correo.
CASHIER_LOGGER vacío Canal de config/logging.php para los mensajes propios del SDK de Stripe.
CASHIER_INVOICE_RENDERER renderizador Dompdf Renderizador de PDF para $invoice->download(). Disponible pero no lo usa ninguna función del kit: la pantalla de cobro lista las facturas y no ofrece PDF.
CASHIER_PAPER letter Tamaño de papel de ese PDF. Mismo estatus.
CASHIER_REMOTE_ENABLED false Si ese PDF puede cargar recursos remotos. Mismo estatus.
STRIPE_WEBHOOK_TOLERANCE 300 Segundos de desfase de reloj aceptados en la firma del webhook. App\Providers\AppServiceProvider lo fija en 300, así que cambiar la variable no tiene efecto.

AppServiceProvider también copia STRIPE_WEBHOOK_SECRET a cashier.webhook.secret, así que las tres variables STRIPE_* se ponen una sola vez, en .env.

La configuración del paquete

config/billing-core.php es el archivo propio del paquete. Rara vez lo cambias; cuando lo hagas, publícalo primero:

php artisan vendor:publish --tag=billing-core-config
Llave Valor por omisión Propósito
user_adapter.model App\Models\User La clase dueña de una cuenta de cobro de usuario.
user_adapter.country_resolver App\Services\CountryDetectionService De dónde sale el país de un visitante cuando el usuario no tiene uno. Ver Detecta el país del visitante.
user_adapter.plan_limits App\Classes\PlanLimits La clase cuyos métodos públicos son las llaves de límite. Ver Límites de plan y consumo.
user_adapter.plan_features App\Classes\PlanFeatures La clase cuyos métodos públicos son las capacidades de los complementos.
adapters vacío Dueños de cobro adicionales declarados sin proveedor, cada uno como ['class' => ..., 'enabled' => ...]. El archivo trae un ejemplo comentado que lee BILLING_SUITE_ENABLED; nada lee esa variable hasta que lo descomentas.
models, morph_types, host clases del paquete Las clases de modelo, los nombres de tipo polimórfico y las clases del proyecto (modelo de evento de webhook, filtro de selección, modelo de categoría) con las que el paquete habla. Déjalas.

config/pricing.php guarda los ajustes comerciales: los intervalos de cobro, la moneda principal y el proveedor de tipos de cambio, cubiertos en Monedas, intervalos y tipos de cambio, y los interruptores de prueba y cupones, cubiertos en Pruebas gratuitas y cupones.

Las tablas

php artisan migrate crea billing_accounts, subscriptions, subscription_items, billing_plans, billing_addons, billing_plan_addon, billing_prices, billing_price_variations, billing_limits, billing_entitlements y currency_exchange_rates. La migración es aditiva: en una base que ya tiene datos de Cashier en users, crea una cuenta de cobro por cada usuario con datos de Stripe y enlaza las suscripciones existentes, sin borrar nada. Revertirla conserva las tablas a propósito.

Después siembra los permisos que necesitan los nuevos recursos, retrieve, create, update y delete sobre billing_plan y billing_addon:

php artisan db:seed --class=PermissionSeeder
php artisan db:seed --class=RoleSeeder

El rol de administrador recibe todos los permisos. Cualquier otro rol necesita que se le otorguen a mano; ver Roles y permisos.

El webhook

Stripe le cuenta a la aplicación qué pasó: se creó una suscripción, se renovó, se canceló, falló un pago. Sin el webhook nada cambia localmente después del Checkout, y el usuario que acaba de pagar sigue viendo "Sin plan".

Registra un endpoint en el Dashboard de Stripe, en Developers, Webhooks:

https://your-project.test/api/stripe/webhook

Debe enviar estos eventos:

Evento Qué hace la aplicación con él
customer.subscription.created Crea la suscripción local y sus elementos.
customer.subscription.updated Actualiza estatus, elementos, fin de prueba y fecha de cancelación; después prende o apaga las capacidades de complementos para que coincidan con los elementos.
customer.subscription.deleted Marca la suscripción como cancelada, registra la fecha y el motivo de cancelación, y retira las capacidades que la suscripción otorgaba.
customer.updated Refresca el método de pago predeterminado guardado localmente.
customer.deleted Cancela las suscripciones locales y limpia los ids de Stripe de la cuenta.
payment_method.automatically_updated Refresca el resumen de la tarjeta cuando la red de tarjetas la actualiza.
invoice.payment_action_required Envía CASHIER_PAYMENT_NOTIFICATION si configuraste una.
invoice.payment_succeeded Lo maneja el comportamiento por omisión de Cashier; el kit no le agrega nada.
invoice.paid Dispara SubscriptionPaid para montos pagados, que paga recompensas de referidos y registra tracking, sincroniza las capacidades de complementos, y es la fuente de la gráfica de ingresos y de las capturas de tipos de cambio.

php artisan cashier:webhook --url=https://your-project.test/api/stripe/webhook crea el endpoint desde la línea de comandos con los primeros ocho eventos; agrégale invoice.paid en el Dashboard después. En cualquier caso, copia el secreto de firma que muestra el Dashboard en STRIPE_WEBHOOK_SECRET.

Registra /api/stripe/webhook, no /stripe/webhook. Cashier también registra el segundo, con su controlador de fábrica, que no sabe nada de complementos, referidos ni de la bitácora de eventos.

Cada petición que recibe el endpoint se guarda en webhook_events con su payload, la respuesta y cualquier error, y se lista en /admin/webhook-events. Esa lista es donde compruebas que Stripe te alcanza; ver Bitácoras y eventos de webhook.

Probarlo en local

Stripe no puede llegar a your-project.test, así que reenvía los eventos con la CLI de Stripe:

stripe login
stripe listen --forward-to your-project.test/api/stripe/webhook

El comando imprime un secreto de firma que empieza con whsec_. Ponlo en STRIPE_WEBHOOK_SECRET mientras corre el listener; no es el mismo del Dashboard. Después suscríbete con una tarjeta de prueba como 4242 4242 4242 4242 y mira llegar los eventos en la terminal y en /admin/webhook-events. Para repetir un evento:

stripe trigger customer.subscription.created

Qué ve el usuario

Cuando BillingPlan::isActive() es verdadero, el usuario con sesión iniciada obtiene:

  • Mis planes en el menú de la cuenta, en /account/plans: las tarjetas de planes con un selector de intervalo, y dos enlaces, Ver historial de pagos (/account/billing) y Métodos de pago (/account/payment-methods). Con los complementos encendidos, el catálogo de complementos se muestra debajo de los planes.
  • Un bloque Plan arriba de la barra lateral de la aplicación con el nombre del plan actual y un enlace Mejorar plan a /account/plans.
  • Tarjetas de consumo en el tablero de la aplicación, una por llave de límite, con las gráficas de uso.
  • Una sección de precios en la página pública de inicio, cuyos botones llevan al registro.
  • Un plan gratuito en el momento en que se registra, cuando existe exactamente un plan gratuito para su país. El registro dispara el evento Registered de Laravel, y el listener del paquete suscribe la nueva cuenta a él, sin tarjeta.
  • El middleware ensure.subscribed en toda ruta bajo /app. Sin una suscripción activa el usuario es enviado a /account/plans con un mensaje pidiéndole una.

En iOS, detectado por la cookie app-platform de la app envuelta, /account/plans muestra un aviso en lugar de los planes, porque ahí no se permiten compras dentro de la app fuera de la App Store.

No hay Stripe Customer Portal. Las tarjetas se agregan, se vuelven predeterminadas y se eliminan dentro de la aplicación, mediante Stripe Elements y un SetupIntent, así que el número de tarjeta nunca toca tu servidor.

Qué ve el administrador

  • Un grupo Planes en la barra lateral del panel con Planes (/admin/billing-plans) y Add Ons (/admin/billing-addons), que conserva su etiqueta en inglés.
  • Una columna Plan actual y un filtro Plan en la lista de usuarios, y un panel Complementos en el formulario de usuario cuando los complementos están encendidos.
  • Tres métricas en el tablero del panel: Ingresos diarios, en la moneda principal, Suscripciones creadas y Suscripciones totales acumuladas, las dos últimas sólo mientras los planes están vivos.
  • La bitácora del webhook en /admin/webhook-events.

Apagarlo

BILLING_CORE_ENABLED=false deshabilita toda ruta, pantalla y comando de cobro mientras el paquete sigue instalado. Para quitar el paquete de forma definitiva, mientras su carpeta aún existe:

composer remove weblabormx/billing-core
php artisan optimize:clear

Después borra la entrada de ruta packages/billing-core del composer.json raíz y la carpeta misma, y corre composer dump-autoload. Ningún paso borra las tablas de cobro ni los identificadores de Stripe; esa es una decisión aparte y deliberada.