Search by

whilesmart / entitlements-cashier

whilesmart

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

Statistics

Installs: 402

Dependents: 1

Suggesters: 1

Stars: 0

Open Issues: 0

0.4.0 2026-10-23 02:31 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

  • BillingProfile is the Cashier customer, keyed to a polymorphic owner. It is separate from the owner model so Cashier's Billable trait never collides with the owner's own invoices() / subscriptions() relations.
  • createCheckout($owner, $plan) opens a Stripe Checkout session for the plan's provider_price_id and 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.