Appearance
Plans
Your catalogue lives in config/laraspring-billing.php. The array key is the plan's logical id, and that id is the only name the rest of your application ever uses: a feature gate says pro, never a Stripe price identifier, so rotating a price in your provider's dashboard is a change to one line here.
php
'plans' => [
'free' => [
'name' => 'Free',
'description' => 'Everything you need to try the product with a small team.',
'free' => true,
'seats' => 3,
'features' => ['Up to 3 members', '1 project', 'Community support'],
],
'pro' => [
'name' => 'Pro',
'featured' => true,
'seats' => 25,
'trial_days' => 14,
'features' => ['Up to 25 members', 'Unlimited projects', 'Audit log'],
'prices' => [
'monthly' => [
'amount' => 2900,
'currency' => 'usd',
'stripe_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_PRICE_ID'),
],
'yearly' => [
'amount' => 29000,
'currency' => 'usd',
'stripe_price_id' => env('LARASPRING_BILLING_PRO_YEARLY_PRICE_ID'),
],
],
],
'enterprise' => [
'name' => 'Enterprise',
'contact' => true,
'contact_url' => 'mailto:sales@example.com',
'features' => ['Unlimited members', 'SAML single sign-on', 'Custom terms'],
],
],What a plan may declare
| Key | Meaning |
|---|---|
name | Shown on the pricing page. |
description | One line under the name. Optional. |
features | Strings for the pricing table, written for a buyer. |
prices | One entry per interval, monthly or yearly. |
free | Selectable, nothing to check out. At most one is meaningful. |
contact | Sold by a conversation. Renders a link, never a checkout. |
hidden | Still resolves for customers on it, not offered to new ones. |
seat_based | Charged per member. See seats. |
seats | How many members the plan allows. Null means unlimited. |
trial_days | Days of trial applied at checkout. Null for none. |
featured | Draws the "most popular" treatment. |
Prices
A price carries an amount in minor units — 2900 is 29.00 — plus a currency, and one <provider>_price_id per provider that sells it.
The amount is here rather than read back from the provider, and that is a trade worth understanding. A price changed in the Stripe dashboard and not here will render wrong on your pricing page. The alternative is an API call on every load of the most-visited screen in your product, against a service that is allowed to be slow and must not be allowed to take your pricing page down. Only the identifier is ever charged, so the worst case is a display bug rather than a billing one.
Adding a second provider is a key rather than a change to the package:
php
'monthly' => [
'amount' => 2900,
'stripe_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_PRICE_ID'),
'lemon_squeezy_price_id' => env('LARASPRING_BILLING_PRO_MONTHLY_LEMON_ID'),
],No identifier reaches the browser
A price identifier in a page prop is billing configuration published to anyone who opens the developer tools. The checkout endpoint takes a plan id and an interval, both validated against this catalogue, and works out what to charge server-side.
The catalogue is validated as it is read
Configuration is a nested array anybody may mistype, and the two places that read it — the pricing screen and the checkout endpoint — are the worst places to find out. So it is parsed into objects at the boundary, once per request, and these are refused loudly:
- a plan that is both
freeandcontact - a
freeorcontactplan that also declares prices - an interval that is not
monthlyoryearly - an
amountthat is not an integer, the usual cause being29.00where2900was meant - a
seatsortrial_daysthat is not a positive integer
A price with no identifier for the configured provider fails when somebody tries to buy it, with a message naming the provider and the interval, rather than rendering a button that quietly does nothing.
Reading the catalogue in your own code
php
use Laraspring\Billing\Support\Billing;
Billing::plans()->offered(); // what a pricing page shows, in order
Billing::plans()->get('pro'); // one plan, throws if it is not configured
Billing::plans()->find('pro'); // one plan, or null
Billing::plans()->free(); // your free plan, if you declared oneBilling::plan() returns the plan the active billable is on, falling back to your free plan. That fallback is what makes "everybody is on a plan" true, so a feature table can be read off it for a paying customer and a free one alike.
Seats
seats caps how many members an organization may have. laraspring/organizations enforces it through a contract in laraspring/core, so neither package depends on the other. See the overview for how a pending invitation is counted.
seat_based marks a plan as charged per member. Today it only tells the quota to read the subscription's quantity instead of the fixed seats above; adjusting the quantity as people join and leave lands with seat billing proper.