crumbls/subscriptions v2.0.1

Plans

A Plan represents a subscription tier -- pricing, billing cycle, trial / grace windows, and an optional subscriber cap.

Pricing

Plan::create([
    'name'        => 'Pro',
    'price'       => 9.99,
    'signup_fee'  => 1.99,
    'currency'    => 'USD',
]);

currency is a label, not a converter. The package never does FX. Display logic, multi-currency catalogs, and conversion belong in your app.

A plan with price = 0 is a free plan. The Plan::free() and Plan::paid() scopes find them.

Billing cycle

'invoice_period'   => 1,
'invoice_interval' => 'month',  // hour | day | week | month | year

Together these define how long one billing period lasts. 1 month is monthly, 1 year is annual, 3 month is quarterly. The interval enum is Crumbls\Subscriptions\Enums\Interval.

When a subscription is renewed ($sub->renew()), starts_at advances by exactly one billing period from the previous ends_at.

Trial period

'trial_period'   => 15,
'trial_interval' => 'day',

When a subscriber starts a subscription, trial_ends_at is set to starts_at + trial_period trial_interval. While now() <= trial_ends_at, $sub->onTrial() returns true.

A subscription on trial is considered active. Feature usage works normally.

Omit both trial_* fields for no trial.

Grace period

'grace_period'   => 7,
'grace_interval' => 'day',

After the billing period ends (ends_at), the subscription enters grace for grace_period grace_interval. During grace, $sub->onGracePeriod() returns true and $sub->active() is still true. Use this window to retry payment, send dunning emails, or block writes without revoking access.

Omit both grace_* fields for no grace window -- the subscription becomes inactive the moment ends_at passes.

Subscriber limit

'active_subscribers_limit' => 100,

Cap how many active subscribers can hold this plan at once. Useful for limited-availability beta plans or capacity-bounded white-glove tiers. Subscribing past the limit throws Crumbls\Subscriptions\Exceptions\SubscriberLimitReachedException.

null (default) means no limit.

Active flag and sort order

'is_active'  => true,
'sort_order' => 10,

is_active = false removes the plan from new-signup flows without deleting it. Existing subscriptions on the inactive plan keep working until they end or get switched. Plan::active() and Plan::inactive() scope queries.

sort_order drives display order on pricing pages and in the Filament panel. Spatie's eloquent-sortable trait does the heavy lifting.

Translatable name and description

name and description are stored as JSON via spatie/laravel-translatable. Set per-locale values:

$plan->setTranslation('name', 'en', 'Pro');
$plan->setTranslation('name', 'es', 'Profesional');
$plan->save();

app()->setLocale('es');
echo $plan->name;  // "Profesional"

Soft deletes

Plans use soft deletes. A deleted plan keeps its row and its existing subscriptions; new signups are blocked. Restore with $plan->restore(). Use $plan->forceDelete() only when you're sure no historical record needs the row.