whilesmart / entitlements-cashier
Laravel Cashier billing adapter for whilesmart/eloquent-entitlements: fulfils the BillingProvider seam so any entitlement owner can check out through Stripe and have its subscription state reflected onto its plan.
Package info
github.com/whilesmartphp/entitlements-cashier
pkg:composer/whilesmart/entitlements-cashier
Requires
- php: ^8.2
- laravel/cashier: ^16.0
- laravel/framework: ^11.0|^12.0
- whilesmart/eloquent-entitlements: ^2.0
Requires (Dev)
- fakerphp/faker: ^1.24
- laravel/pint: ^1.22
- orchestra/testbench: ^9.0|^10.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-dev
- 0.4.0
- 0.3.0
- 0.2.0
- 0.1.1
- 0.1.0
- dev-test/end-promotion-codes
- dev-main
- dev-release-0.4.0
- dev-feat/offers
- dev-feat/checkout-promotion-codes
- dev-release-0.3.0
- dev-fix/subscription-access-lifecycle
- dev-fix/trial-shows-the-days-asked-for
- dev-release-0.2.0
- dev-chore/release-0-2-0
- dev-feat/checkout-trial-days
- dev-release-0.1.1
- dev-feat/plan-price-sync
- dev-release-0.1.0
This package is auto-updated.
Last update: 2026-10-23 02:58:25 UTC
README
A Laravel Cashier billing adapter for whilesmart/eloquent-entitlements. It fulfils the entitlements BillingProvider seam so any entitlement owner (a workspace, team, or user) can check out through Stripe and have its subscription state reflected onto its plan, without each app rebuilding the wiring.
Why
eloquent-entitlements owns the seam (BillingProvider) and stays payment-SDK-neutral. This package is the Cashier implementation of that seam, shipped once so a host does not hand-roll a billable model, a provider, a webhook listener, and migrations in every project.
Install
composer require whilesmart/entitlements-cashier
php artisan migrate
Cashier reads STRIPE_KEY, STRIPE_SECRET, and STRIPE_WEBHOOK_SECRET from the environment. Point your Stripe webhook at Cashier's route (/stripe/webhook). Set each paid plan's Stripe Price ID on its provider_price_id.
That is the whole integration. The service provider binds the BillingProvider, makes the billing profile Cashier's customer model, registers the webhook listener, and loads its migrations.
How it works
BillingProfileis the Cashier customer, keyed to a polymorphicowner. It is separate from the owner model so Cashier'sBillabletrait never collides with the owner's owninvoices()/subscriptions()relations.createCheckout($owner, $plan)opens a Stripe Checkout session for the plan'sprovider_price_idand returns the redirect URL.- The webhook listener retrieves the current subscription from Stripe before updating access. Trial and active subscriptions carry an expiry. Failed payments suspend access immediately; a recovered payment restores it. Cancellation at period end preserves access until expiry. Older events for replaced subscriptions cannot overwrite the replacement.
Prices
A plan's amount is the single source of truth; you do not maintain a Stripe price id. Put the amount on the plan's metadata.price (amount_cents, currency, interval), then run:
php artisan entitlements-cashier:sync-prices
For each priced plan this creates a matching Stripe Price and records its id on the plan's provider_price_id. Stripe Prices are immutable, so a new one is created only when the amount, currency, or interval changes; the previous stays for existing subscribers. PlanPriceSync is also resolvable if you prefer to run it from your own seeder.
Configuration
Publish with php artisan vendor:publish --tag=entitlements-cashier-config.
| Key | Purpose |
|---|---|
billing_profile_model |
Swap the Cashier billable model for a host subclass. |
profiles_table |
The billing profiles table name. |
success_url / cancel_url |
Where Stripe Checkout returns; defaults to the app URL. |
default_plan |
Plan used when no subscription grants access. A host-provided plan source retains precedence. |
register_migrations |
Set false to bring your own migrations. |
promotion_codes |
Show the promotion code box on checkout. On by default. |
Discount codes
PromotionCodes makes the codes a buyer types at checkout, as Stripe promotion codes:
$codes = app(\Whilesmart\EntitlementsCashier\Discounts\PromotionCodes::class); $codes->create('PILOT20', PromotionCodes::REPEATING, percentOff: 20, months: 3); $codes->create('ADA50', PromotionCodes::FOREVER, percentOff: 50, for: $owner); $codes->all(); $codes->stop($id); $codes->end($id);
stop keeps anybody new from using a code, and subscriptions that already carry it
keep it. end also takes it off those subscriptions from their next bill, which is
the only way a code that lasts forever ends. A code held for one owner needs that
owner to have a Stripe customer, which their first checkout makes.
Testing
composer test
Checkout and webhook access
A signing secret is required. Unsigned webhooks are rejected even when the host has not configured billing. Subscribe to checkout completion, subscription creation, updates and deletion, invoice payment failure and successful invoice payment events. Stripe retries a failed delivery; the listener retrieves current state on each retry.
An owner reuses an unexpired checkout session. Existing subscriptions are managed through the customer portal. Trial eligibility is claimed only when Stripe confirms an actual trial, and the trial lasts the configured number of days from activation.
Checkout recovery
Apply the package migrations before enabling checkout. Checkout requests are saved before submission. A retry after a lost response reuses the original request and Stripe idempotency key, including its trial length and return URLs. Changing plans first recovers and expires the unfinished checkout. expireCheckout($owner) also recovers an interrupted checkout before canceling it.
Use a shared cache store with atomic locks when checkout runs on multiple application servers. Hosts can customize checkoutParameters before a new request is saved; retries keep the saved parameters.