Add flexible billing mode support#1830
Conversation
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
1ecbbb1 to
86f4ce6
Compare
- 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
|
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. |
|
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. |
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.cloverand 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-inwithBillingMode('flexible')— per-subscription overridewithProrationDiscounts('itemized'|'included')— invoice controlmigrateToFlexibleBillingMode()— one-way migration via /migrate endpointbilling_modecolumn on subscriptions table for local lookupsSubscription Schedules
Cashier::useSubscriptionScheduleModel()Quotes
Cashier::useQuoteModel()Billing Credits
hasSufficientCredits(),availableCredits(),calculateCreditApplication()creditBalance()/debitBalance()Usage Thresholds
setUsageThreshold()isExceeded(),usagePercentage(),overage()calculationsRate Cards
Webhook Handlers
subscription_schedule.created/updated/canceled/completed/releasedquote.finalized/accepted/canceledBackwards Compatibility
Fully backwards compatible:
classic— existing apps unchangedbilling_modecolumn is nullableDocumentation
Full guide at
docs/flexible-billing.mdwith 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
Credits
Based on #1772 by @Diddyy with review feedback from @crynobone, @yoeriboven, @j3j5, and @Arkitecht.
/cc @taylorotwell @crynobone @driesvints