Límites de plan y consumo
Declara qué puede encender un plan y cuánto de algo permite, fija los números por plan y hazlos valer en tu propio código.
Un plan vende dos tipos de cosas: capacidades, que están encendidas o apagadas, y límites, que son números. Ambas se declaran en código y se les pone precio en el panel de administración. Esta guía cubre las dos clases que escribes, las pantallas que alimentan, cómo se calcula el total de una cuenta y la verificación que agregas antes de crear un registro.
Tres palabras
| Palabra | Qué es | Dónde vive |
|---|---|---|
| Capacidad (feature) | Un sí o no: ¿esta cuenta tiene white_label? |
Se declara como método de App\Classes\PlanFeatures. Se guarda como feature_key en un complemento. La responde la cuenta de facturación. |
| Entitlement | Una razón por la que una cuenta tiene una capacidad: la activó gratis, está en prueba o la compró de por vida. | Una fila en billing_entitlements. La inclusión en un plan y las líneas de la suscripción no son filas; se leen en vivo. |
| Límite | Un número: cuántas activities puede tener esta cuenta. |
Se declara como método de App\Classes\PlanLimits. Cada plan y cada complemento guarda un valor para él en billing_limits. |
Una capacidad se resuelve a partir de todas las formas en que pudo llegar; un límite es una suma. El kit no hace valer ninguno de los dos sobre tus propios registros: tú preguntas antes de crear.
Capacidades
Declara la primera
app/Classes/PlanFeatures.php extiende App\Classes\BasePlanFeatures. Un método público por capacidad. El nombre del método es la clave que el administrador elige al crear un complemento, y el valor de retorno dice si la capacidad ya está liberada en código, así que puedes vender algo en preventa que todavía no encienda nada.
Weblabor Base entrega la clase sin métodos a propósito: una instalación limpia no tiene capacidades propias, así que el formulario de complementos no muestra el campo Capability y el catálogo está vacío hasta que tu proyecto declara una. Agrega la primera así:
namespace App\Classes;
class PlanFeatures extends BasePlanFeatures
{
public function advanced_reports()
{
return true;
}
public function white_label()
{
return false; // en preventa, todavía nada lo lee
}
}
Usa snake case: la clave se guarda tal como la escribes y las etiquetas se derivan de ella.
BasePlanFeatures lee la clase por reflexión, así que no hay ninguna lista que mantener:
| Método | Devuelve |
|---|---|
PlanFeatures::availableFeatures() |
Cada método público no estático salvo el constructor: ['advanced_reports', 'white_label']. |
PlanFeatures::featureOptions() |
Clave a etiqueta mediante Str::headline: ['advanced_reports' => 'Advanced Reports', ...]. Las opciones del select Capability. |
PlanFeatures::isReleased('white_label') |
true solo cuando la clave está declarada y su método devuelve un valor verdadero. |
La clase está conectada en config/billing-core.php bajo user_adapter.plan_features. Solo cambias esa clave cuando mueves la clase.
Pregunta si una cuenta tiene una
La cuenta de facturación es lo único que conoce todas las fuentes, así que pregúntale a ella:
$account = $user->billingAccount;
$account->hasAddOn('advanced_reports'); // true o false
$account->addOnSource('advanced_reports'); // 'lifetime', 'plan', 'subscription', 'trial', 'free' o null
$account->addOnQuantity('extra_seats'); // unidades que tiene, 0 cuando no la tiene
App\Traits\HasFeatures, que usa App\Models\User, convierte una columna JSON features en arreglo y ofrece hasFeature($key), enableFeature($key) y disableFeature($key). Es una caché de lectura: la cuenta la reescribe después de cada otorgamiento, revocación y webhook de Stripe mediante refreshFeatureCache(), en billing_accounts.features y, cuando la tabla del propietario tiene una columna features, también en el propietario. La tabla users de Weblabor Base no tiene esa columna, así que $user->hasFeature() no tiene nada que leer y $user->enableFeature() intentaría escribir una columna que no existe. Agrega la columna cuando tu producto necesite la caché en el usuario; si no, lee hasAddOn() en la cuenta. Las cinco fuentes, las columnas de billing_entitlements y grantAddOn() están en Vende complementos.
Límites
Declara un medidor
app/Classes/PlanLimits.php extiende App\Classes\BasePlanLimits. Un método público por límite: el nombre del método es la clave del límite y el método devuelve cuánto ha consumido el usuario con sesión iniciada. Un segundo método con el mismo nombre más Daily, que recibe una fecha from y una to, devuelve la serie diaria que dibuja la gráfica de consumo. El kit trae un ejemplo resuelto:
namespace App\Classes;
use Carbon\Carbon;
class PlanLimits extends BasePlanLimits
{
public function activities()
{
return auth()->user()->activities()->count();
}
public function activitiesDaily(Carbon $from, Carbon $to)
{
return auth()->user()->activities()
->whereBetween('created_at', [$from, $to])
->selectRaw('DATE(created_at) as date, COUNT(*) as count')
->groupByRaw('DATE(created_at)')
->pluck('count', 'date');
}
}
Dos cosas que conservar del ejemplo. El medidor no recibe argumentos: para el adaptador de usuario el paquete lo llama sin ninguno, así que mide a auth()->user(). Y el método diario devuelve una colección con la fecha Y-m-d como clave y el conteo como valor; los días sin filas simplemente no aparecen.
BasePlanLimits lee la clase por reflexión:
| Método | Devuelve |
|---|---|
PlanLimits::availableLimits() |
Cada método público no estático salvo el constructor, dailyUsage y cualquier nombre que termine en Daily: ['activities']. |
PlanLimits::limitOptions() |
['activities' => 'activities'], las opciones del select Limit Key en el panel de administración. La clave se muestra tal como está escrita. |
app(PlanLimits::class)->dailyUsage('activities', $from, $to) |
Llama a activitiesDaily(). Una colección vacía cuando no existe el método Daily, así que la gráfica sale plana, no rota. |
La clase está conectada en config/billing-core.php bajo user_adapter.plan_limits. Un límite que ningún plan usa es inofensivo: se muestra como ilimitado.
Fija el valor por plan
Cada plan y cada complemento tiene una tarjeta Limits en su formulario, en /admin/billing-plans/{id}/edit y /admin/billing-addons/{id}/edit. Add Limit agrega una fila:
| Campo | Regla |
|---|---|
| Limit Key | Un select sobre PlanLimits::limitOptions(). Cuando el Context del formulario es User, las opciones salen de billing-core.user_adapter.plan_limits; Team, de billing-team.plan_limits; Both las combina. Cada clave puede aparecer una sola vez por plan: "Each limit key can only be used once." |
| Limit Value | Un entero, al menos 0. En un plan, como máximo 999999999. |
Guardar escribe las filas en billing_limits (limitable_type, limitable_id, limit_key, limit_value, con soft deletes): las filas que quitaste se borran, las existentes se actualizan y las nuevas se crean. Nada se envía a Stripe por un límite; solo los precios viajan.
Un valor de 0 es un límite real de cero, no "ilimitado". Para dejar un límite abierto, no agregues la fila.
Qué recibe una cuenta
$user->getLimitTotal('activities') recorre la suscripción default de la cuenta:
límite del plan para la clave
+ límites de los complementos que son líneas de la suscripción
+ límites de los complementos que el plan incluye y que no son ya líneas
Devuelve null cuando ninguno de los tres define la clave, y la pantalla muestra ∞. Los complementos que se tienen como entitlements free, trial o lifetime no suman a un límite.
Sin suscripción la respuesta es 0 mientras los planes estén activos, y null cuando no lo están (bandera apagada, sin STRIPE_SECRET o sin ningún plan todavía), así que una cuenta sin suscripción está por encima de todos los límites. Es intencional: el registro suscribe a todos al plan gratuito, y a un usuario que no está en ningún plan hay que mandarlo a /account/plans, que es lo que ya hace el middleware ensure.subscribed para las rutas de /app.
El total se guarda en la caché del store database durante un día, bajo una clave que incluye la fecha de actualización del item de suscripción más reciente. Cambiar el valor de un límite del plan en el panel de administración no cambia ningún item, así que los suscriptores existentes ven el total anterior hasta que la caché expira o su suscripción cambia. Para verlo de inmediato:
php artisan cache:clear database
$user->getLimitUsed('activities') llama a tu PlanLimits::activities(); una clave sin método cuenta como 0. $user->reachedLimit('activities') es true cuando el total no es null y lo consumido es mayor o igual que él.
Qué ve el usuario
El dashboard en /app muestra un bloque de consumo cuando los planes están activos: una tarjeta por clave de PlanLimits::availableLimits(), con la clave como título (activities se lee "Activities", traducido mediante lang/), el conteo consumido y el total o ∞. Cuando existe un total la tarjeta dibuja una barra de progreso, roja en cuanto se alcanza el límite.
Al hacer clic en una tarjeta se abren dos gráficas de ese límite, tomadas de tu método <key>Daily(): una de columnas con el consumo diario y una de línea con el consumo acumulado, sobre un rango From y To que empieza en el mes en curso y se puede cambiar ahí mismo. Otro clic en la tarjeta las cierra. El componente es App\Livewire\App\UsageLimits; el dashboard lo incluye como <livewire:app.usage-limits />.
Las tarjetas de plan en /account/plans muestran las viñetas de texto del plan, no sus límites. La página de detalle del complemento, /account/add-ons/{id}, lista los límites del complemento bajo "What it includes", como el valor seguido de la clave en minúsculas.
Haz valer un límite en tu código
El kit verifica un límite exactamente en un lugar: cuando una suscripción se mueve a otro plan (un cambio de plan, o el plan gratuito en el registro), cada límite del plan destino se compara con el consumo actual y el movimiento se rechaza con "You have exceeded the limit for :key (:current / :max)." cuando el consumo está por encima del valor. Los límites de los complementos no forman parte de esa verificación, y nada impide que un usuario cree el registro que se pasa.
Así que la verificación antes de crear algo es tuya. Pregúntale al usuario antes de escribir:
use Livewire\Component;
use WireUi\Traits\WireUiActions;
class CreateActivity extends Component
{
use WireUiActions;
public function save()
{
if (auth()->user()->reachedLimit('activities')) {
$this->notification()->error(
title: __('Limit reached'),
description: __('Your plan allows :max activities. Upgrade to add more.', [
'max' => auth()->user()->getLimitTotal('activities'),
])
);
return;
}
// crea el registro
}
}
Usa reachedLimit() en lugar de comparar los números por tu cuenta, para que null siga siendo ilimitado. Muestra el total y un camino a la página de planes, /account/plans, en vez de un rechazo a secas, y pon la frase en lang/ como cualquier otro texto. Cuando el registro lo crea algo distinto del usuario con sesión, un job o un webhook, recuerda que el medidor lee auth()->user(): ahí mide con tu propia consulta en lugar de llamar a PlanLimits.