← Larameter 5 / 12

Plans

A plan is one allowance figure, some ceilings, and some switches. Everything else about it, its name, its price, its Stripe identifier, its marketing copy, is yours and the package neither reads nor minds.

Plans are optional. An account with no plan is valid and spends purchased credits only, which is what you want if you sell bundles rather than subscriptions.

Define them once, where they already live

Most projects already have a plans file before they install anything like this. Point the package at it and a plan stays defined in one place, with the commercial half beside the metered half:

// config/larameter.php
'plans_from' => 'plans.tiers',
// config/plans.php
'tiers' => [
    'pro' => [
        'name'  => 'Pro',
        'price' => 59_00,
        'stripe_price_id' => env('STRIPE_PRICE_PRO'),

        'credits_monthly' => 50_000,
        'limits'   => ['members' => 1, 'cases' => -1],
        'features' => ['api_access' => false, 'own_cases' => true],
    ],
],

The package reads the allowance figure, limits and features, and ignores the rest.

credits_key names the key holding the allowance, credits_monthly by default, and it may name a nested one for a plans file that already had it somewhere else:

'credits_key' => 'limits.credits_monthly',

That one is read through dot notation, unlike the rate table. See Prices and rates for why the two differ.

Three defaults that read in different directions

On purpose, because they are three different kinds of thing:

No allowance key at all No credits. Credits are what you sell, so a plan that does not mention them does not include any.
A features key you never listed Off. A feature is something a plan unlocks, so one nobody wrote down was never granted.
A limits key you never listed Unlimited. These are restrictions, and a package you just installed should not refuse to create users on its own opinion.

-1 is unlimited anywhere, which is not the same as 0. A limit of 0 forbids the resource entirely; an allowance of -1 is an uncapped one.

Reading a plan

$org->plan();                       // a Plan, never null
$org->plan()->exists;               // false when no provider answered
$org->plan()->handle;               // 'pro', or '' when there is none
$org->plan()->name;                 // 'Pro', falling back to the handle
$org->plan()->price;                // whatever unit you wrote it in; never charged
$org->plan()->is('pro');
$org->plan()->allows('api_access');
$org->plan()->limit('members');     // -1 when unlimited
$org->plan()->credits();            // the whole allowance, before any window narrows it
$org->plan()->creditsIn('weekly');  // that figure times the window's share
$org->plan()->get('stripe_price_id');
$org->plan()->toArray();            // the raw definition

Data is a property, a question is a method. name is a property and not a method because a plan name is a product name: Pro, Max, Hyper Team. Nobody translates those, any more than they translate the name of the application.

plan() never returns null. When nothing resolves, you get a Plan with an empty handle, exists false, no features and no ceilings, so calling code does not need a null check before every question.

A Plan is generic. A handle, a name, an allowance and some ceilings, and it reads the same whether it was resolved from a subscription, from a column of yours, or from a default. What a provider had to know in order to answer stays inside that provider. See Plan providers.

get() and the rest of the definition

Anything else you keep in the plan is reachable, through dot notation, without the package having to know it exists:

$org->plan()->get('trial_days', 0);
$org->plan()->get('support.sla');

This is what makes plans_from worth using. The plan file stays the single description of a tier, and the package is one of several readers of it.

Changing plan

Changing plan does not restart the windows. An upgrade raises the ceiling over what has already been spent, rather than handing a second allowance to whoever works out they can upgrade and downgrade in the same afternoon.

The mechanism depends on where the plan comes from. With a subscription, you change the subscription and the next request resolves differently, with nothing here to call. With an override column, you write the column. And when nothing can resolve at all, there is:

$org->credits()->setPlan('pro');
$org->credits()->setPlan(null);

That writes the plan column on the account row and clears the memoised plan on the model. It is the fallback, not the source: with HasPlans in place, the stored value is only reached when no provider answers before StoredPlanProvider.

Unlimited allowances

A plan whose allowance figure is negative is uncapped. creditsIn() then returns -1 for every window, WindowUsage::isUnlimited() is true, remaining() on that window is PHP_INT_MAX, and percentUsed() is 0.0 so that a progress bar does not have to special case it.

Usage rows are still written. An uncapped plan is still metered, which is what you want the day somebody asks what the largest customer actually costs to serve.