Skip to content

Add flexible billing mode support#1830

Closed
JoshSalway wants to merge 5 commits intolaravel:16.xfrom
JoshSalway:feature/flexible-billing-clean
Closed

Add flexible billing mode support#1830
JoshSalway wants to merge 5 commits intolaravel:16.xfrom
JoshSalway:feature/flexible-billing-clean

Conversation

@JoshSalway
Copy link

This PR adds support for Stripe's flexible billing mode to Laravel Cashier.

Flexible billing is Stripe's modern billing mode, already the default on API version 2025-09-30.clover and later. It enables usage-based pricing, hybrid subscriptions (fixed + metered on one subscription), improved proration control, and advanced subscription lifecycle management.

Open to feedback, suggestions, or improvements on any of this.

Features

Flexible Billing Mode

  • Cashier::defaultBillingMode('flexible') — global opt-in
  • withBillingMode('flexible') — per-subscription override
  • withProrationDiscounts('itemized'|'included') — invoice control
  • migrateToFlexibleBillingMode() — one-way migration via /migrate endpoint
  • billing_mode column on subscriptions table for local lookups
  • Billing mode flows through SubscriptionBuilder, CheckoutBuilder, QuoteBuilder, ScheduleBuilder

Subscription Schedules

  • SubscriptionSchedule model and SubscriptionScheduleBuilder
  • Multi-phase lifecycle management with cancel, release, update
  • Swappable via Cashier::useSubscriptionScheduleModel()

Quotes

  • Quote model and QuoteBuilder
  • Full lifecycle: draft, finalize, accept/cancel
  • PDF download, line items, metadata, expiry
  • Swappable via Cashier::useQuoteModel()

Billing Credits

  • hasSufficientCredits(), availableCredits(), calculateCreditApplication()
  • Builds on existing creditBalance()/debitBalance()

Usage Thresholds

  • Database-backed usage monitoring with setUsageThreshold()
  • isExceeded(), usagePercentage(), overage() calculations

Rate Cards

  • Local tiered (graduated/volume), package, and flat pricing calculations

Webhook Handlers

  • subscription_schedule.created/updated/canceled/completed/released
  • quote.finalized/accepted/canceled
  • Custom events dispatched for each state change

Backwards Compatibility

Fully backwards compatible:

  • No existing method signatures changed
  • No methods removed
  • Default billing mode is classic — existing apps unchanged
  • New billing_mode column is nullable
  • All 59 original unit tests pass unmodified

Documentation

Full guide at docs/flexible-billing.md with code examples for every feature.

Interactive Demo

A standalone Laravel 13 demo app runs 14 scenarios against the real Stripe API:
https://github.com/JoshSalway/cashier-flexible-billing-demo

Live: https://cashier-flexible-billing-demo-master-hzg0rn.laravel.cloud

Tests

  • 177 unit tests, 309 assertions
  • 47 feature tests against real Stripe API
  • PHPStan clean

Credits

Based on #1772 by @Diddyy with review feedback from @crynobone, @yoeriboven, @j3j5, and @Arkitecht.

/cc @taylorotwell @crynobone @driesvints

Introduces Stripe's flexible billing mode to Laravel Cashier, enabling
usage-based pricing, hybrid subscriptions, and improved proration
handling.

Core changes:
- ManagesBillingMode trait shared across SubscriptionBuilder,
  Subscription, CheckoutBuilder, and all new builders
- Cashier::defaultBillingMode('flexible') for global configuration
- withBillingMode('flexible') for per-subscription override
- withProrationDiscounts('itemized'|'included') for invoice control
- migrateToFlexibleBillingMode() via Stripe's /migrate endpoint
- billing_mode column on subscriptions table for local lookups
- usesFlexibleBilling() checks local DB first, falls back to API
- Validation: billing thresholds incompatible with flexible mode
- Guards: canceled/incomplete subs cannot migrate, idempotent migration
- billing_mode correctly placed per Stripe API (top-level on create,
  NOT on update, inside subscription_data on quotes/checkout)

Requires Stripe API version 2025-06-30.basil or later.
Compatible with stripe-php ^17.3.0 (v17.4.0+ for migrate endpoint).

Based on PR laravel#1772 by @Diddyy with review feedback from @crynobone.
Subscription Schedules:
- SubscriptionSchedule model with status management, cancel, release,
  update, phase management, and Stripe sync
- SubscriptionScheduleBuilder with fluent API for phases, billing mode,
  start dates, end behavior, createFromSubscription()
- ManagesSubscriptionSchedules trait on Billable
- Swappable via Cashier::useSubscriptionScheduleModel()

Quotes:
- Quote model with full lifecycle (draft/open/accepted/canceled),
  finalize, accept, cancel, PDF download, and Stripe sync
- QuoteBuilder with line items, descriptions, headers/footers, expiry,
  billing mode, discounts, and tax rates
- ManagesQuotes trait on Billable
- Swappable via Cashier::useQuoteModel()

Billing Credits:
- ManagesBillingCredits trait with hasSufficientCredits(),
  availableCredits(), calculateCreditApplication(),
  addBillingCredits(), deductBillingCredits()
- Builds on existing creditBalance()/debitBalance() in ManagesCustomer

Webhook Handlers:
- subscription_schedule.created/updated/canceled/completed/released
- quote.finalized/accepted/canceled
- Custom events dispatched for each state change

Migrations:
- subscription_schedules table
- cashier_quotes table (prefixed per review feedback)
Usage Thresholds:
- UsageThreshold model for database-backed usage monitoring
- setUsageThreshold(), getUsageThreshold(), removeUsageThreshold()
  on ManagesUsageBilling trait
- isExceeded(), usagePercentage(), overage() calculations
- cashier_usage_thresholds table

Rate Cards:
- RateCard model for local pricing calculations (not synced with
  Stripe — intentional design for display and estimation)
- Tiered pricing (graduated and volume modes)
- Package pricing with round-up
- Flat rate pricing
- active() and forProduct() scopes
- cashier_rate_cards table

Model Factories:
- QuoteFactory, SubscriptionScheduleFactory, UsageThresholdFactory,
  RateCardFactory for consumer testing
Unit Tests (177 total, 309 assertions):
- BillingModeTest: trait logic, defaults, overrides, proration discounts
- SubscriptionBuilderTest: billing mode in payload, thresholds guard
- SubscriptionTest: migration guards, billing mode methods
- CheckoutBuilderTest: billing mode inheritance
- SubscriptionScheduleTest/BuilderTest: status, phases, validation
- QuoteTest/BuilderTest: status, amounts, builder API
- BillingCreditsTest: calculations, coverage, balance checks
- UsageThresholdTest: exceeded, percentage, overage
- RateCardTest: tiered, volume, package, flat pricing
- WebhookControllerTest: all 8 new handlers callable

Feature Tests (47 total, against real Stripe API):
- FlexibleBillingTest: core billing mode, credits, schedules, quotes
- FlexibleBillingLifecycleTest: 8 end-to-end scenarios
- FlexibleBillingWebhookTest: webhook handlers with DB persistence
- FlexibleBillingCheckoutTest: checkout session billing mode

Documentation:
- docs/flexible-billing.md with full API reference, code examples,
  migration guide, webhook reference, and chaining examples
@JoshSalway JoshSalway force-pushed the feature/flexible-billing-clean branch from 1ecbbb1 to 86f4ce6 Compare March 20, 2026 16:55
- Subscription::usesFlexibleBilling(): added isset() null check and
  strict === comparison on Stripe billing_mode object to prevent
  fatal errors on subscriptions without billing_mode set
- UsageThresholdFactory: fixed column name from 'metric' to 'meter_id'
  and added missing 'period' column to match migration schema
- StyleCI: single-quoted expectExceptionMessage strings
@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

I appreciate you taking the time to submit this; however, it appears this contribution may have been primarily AI-generated without careful human review and consideration.

We've found that AI-generated code often doesn't align well with Laravel's conventions, architectural decisions, and the specific context of what we're trying to accomplish with the framework. Quality contributions require thoughtful human insight into the codebase.

If you're interested in contributing to Laravel, I'd encourage you to familiarize yourself with the existing codebase, engage with the community, and submit PRs that reflect your own understanding and careful consideration of the problem you're solving.

@JoshSalway
Copy link
Author

Thanks for the feedback, @taylorotwell. Understood. I did use AI assistance to help build this out, and I appreciate you being upfront about where you stand on that.

For context, I need flexible billing support across several of my own projects and built this as a proof of concept. Given the scope of flexible billing, the documentation alone is extensive and there are a lot of edge cases, so it would have been very difficult to tackle without AI assistance.

I'll continue working on it via a fork and testing it against my own Stripe account. If it matures to a point where it might be worth revisiting, I'll reach out. Thanks again for your time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants