pushery/billing-for-laravel
pushery/billing-for-laravel is a Laravel package for adding billing and subscription features to your application, with configurable plans, invoicing, and payment handling for a lightweight, straightforward integration.
BillingManager::forAccount(PaymentAccount $account) plus per-account credentials, test mode, and Mollie profile means the product can move from "the platform collects and settles later" to "each shop charges into its own Mollie or Stripe account." That is a foundational decision (platform-of-record vs. connected accounts) and it is now a configuration choice rather than a custom build.billing.collection_timing (BILLING_COLLECTION_TIMING) now decides when a new subscription on a package-billed driver is charged: in_advance (the default) bills a period when it begins, the first one at the next billing:run after the mandate exists or when the trial ends; in_arrears keeps the old behaviour of charging at the period's end. This is a pricing-and-cash-flow decision, not a technical one — a yearly subscriber now pays on day one instead of after a year — and it has downstream consequences for dunning (a failed advance invoice is a failed upcoming period, not a past one), refunds, revenue recognition, and churn accounting. Each subscription keeps the timing it started with, so nothing already running changes.PausesSubscriptions::pause() / unpause() stops collection on an active, non-ending subscription and grants no access until resumed. On package-billed drivers the paused days are added back to the period; a cancellation while paused ends it immediately; Stripe maps to pause_collection with void. Decide whether "pause" appears in the cancel flow, which plans may pause, and what (if anything) a paused customer can still see or do. SubscriptionPaused / SubscriptionUnpaused exist precisely so access locks and notices are your policy, not the package's.CancellationButton::declare() accepts a cancellation declared on your own pages (signed in or not) as a DeclaredCancellation per § 312k BGB, and withdrawals/prorated cancellations refund the period in progress now that ReadsSubscriptionPayments answers with the order that paid it. Decide whether you sell into DE/EU, whether the statutory button ships, and who signs off. Compliance work moves from legal backlog to configuration plus copy review.DirectDebitMandates::text() returns the mandate wording for the payer's country under a reference and give() registers it from name + IBAN, with IBAN country/length/check-digit validation before anything reaches Mollie, the exact displayed text recorded, and consent stored once (text, reference, account holder, notice period, when and from where) with erasure unlinking it. This enables a mandate-first or no-card signup path for EU customers; the shipped German wording covers AT, DE, LI, LU, and billing.direct_debit.mandate_wordings takes your own SepaMandateWording elsewhere.CostCalculator::costOf($model, TokenUsage::fromLaravelAi($response->usage)) prices a call across five token buckets from a dated billing.ai.rates table, and SpendBrake::spend($provider, $cost, owner: $user) enforces per-provider and per-owner daily caps atomically (check-and-count in one statement). The decision this unlocks is whether AI features ship with hard margin protection and per-user fairness limits from day one rather than a post-hoc cost surprise.UsageGrants are drawn first-expiring-first, and only then are non-expiring bought prepaid units spent, so nothing that expires goes unused; billing:usage:flush writes off expired remainders each minute as usage.grant_expired; a reference makes a grant idempotent per owner and meter. A blocking meter may declare grace, letting work under way finish past the ceiling, with the excess owed as debt on the balance row and repaid by the next grant before any of its units can be spent. That is the "does the plan reset, and is a hard stop a cliff or a soft overage?" decision, made explicit.ChargeOptions still carries return/cancel/webhook URLs, locale, methods, metadata, order lines, addresses, and the card token; accountCsp() and billing.redirect_hosts still force a call on custom domains and embedded card fields.billing:money-back:reconcile, RefundFailed, ChargebackReversed, RefundResult::failureCode/failureMessage, and the delivery ledger remain off the roadmap. New: billing:doctor names every Stripe webhook event type an endpoint at a package address is not subscribed to and fails while one is missing — catching the class of silent breakage where, for example, a missing invoice.upcoming pushed usage billing a cycle late without a word. The package still stops at emitting events and recording outcomes.BILLING_METERING_*, grace may be 0, backoff/stall at least 1) and a payment account's Molli client timeouts, connect timeout, and retries — separate values for web requests and for queue workers/scheduler/Artisan — are now environment-set, with invalid or sub-second values falling back to shipped defaults. Decide whether operations tunes these in production or whether they are fixed in config review.Consider adoption when:
billing.webhook_path/{provider}/{account}), per-account verification secrets, and per-account replay.AccountPaymentChanged with pending/authorized/paid/partially-or-fully-refunded/failed-refund/chargeback/failed/canceled/expired, plus refunded and charged-back amounts, idempotency key, metadata).laravel/ai and want per-call cost attribution plus daily caps per provider and per owner before you turn them on for customers.Look elsewhere when:
cancel() throws SaleNotCancelable because an open terminal payment cannot be taken off remotely; the buyer must press cancel on the device, and the terminal stays busy until the payment expires.collection_timing, proration, grants, and the pause semantics are driver-specific value you would not exercise.extendAccountCsp() / extendAccountPaymentMethods() / ReportsMoneyBackOutcomes / CollectsDirectDebits seams.For executives — one paragraph. This release turns the package from a payments layer into a billing policy layer. We can now choose to collect subscriptions in advance or in arrears, pause a subscription instead of losing the customer, settle upgrades, downgrades, and seat changes at the moment they happen with correct credit, hand out allowances that expire and prepaid units that don't, and let a blocking meter grant a small grace so work in flight finishes rather than failing at its last unit. EU selling becomes a product feature rather than a legal backlog item: SEPA mandates can be taken on our own form without a payment, with the exact wording, consent, and erasure trail stored, and the statutory § 312k BGB cancellation declaration plus withdrawal refunds of the period in progress are supported. If we ship AI features, per-call cost attribution and hard daily caps per provider and per owner are available before launch, so margin protection is configuration. Operations can tune metering backoff, stall, and grace and the Mollie client's timeouts and retries from the environment, and billing:doctor now fails the build when a Stripe endpoint is missing an event type we depend on — the class of silent failure that previously delayed usage billing by a cycle with no error anywhere. In short, it removes advance/arrears billing, pause, proration, mandate onboarding, usage allowances, AI spend control, and webhook drift detection from our roadmap.
For engineering — what to plan for. One breaking change and a batch of migrations. (1) billing.collection_timing defaults to in_advance, so a new subscription on a package-billed driver is charged at the next billing:run after the mandate exists or when the trial ends, rather than at period end. Existing subscriptions are grandfathered into arrears, and the timing is recorded per subscription by 2026_10_15_000163_add_collection_timing_to_billing_subscriptions_table — so the risk is confined to new signups, which is exactly what staging tests miss if they use seeded old data. Test first-invoice timing at signup and at trial end, dunning on a failed advance invoice, and the rule that a period-end downgrade is applied before the next period is charged. (2) Run and review six migrations: collection_timing, paused_at, mandate consents, direct debit notices, AI spend, and expiring grants/grace. (3) Publish or set the new environment variables — BILLING_METERING_BACKOFF_SECONDS, BILLING_METERING_STALL_HOURS, BILLING_METERING_CLOSED_PERIOD_GRACE_MINUTES (grace may be 0, backoff and stall need ≥ 1), and BILLING_MOLLIE_ACCOUNT_REQUEST_TIMEOUT / _CONNECT_TIMEOUT / _RETRIES plus the three BILLING_MOLLIE_ACCOUNT_BACKGROUND_* counterparts; invalid values silently keep shipped defaults, so assert the resolved values in a boot test. (4) Pause requires access gating in the app, implementation of PausesSubscriptions on any custom driver, and void semantics on Stripe; cancellation while paused ends immediately — verify that path. (5) The direct debit flow checks that text() matches exactly what the form displayed, validates IBAN before calling Mollie, and throws DirectDebitMandateRefused with a reason you bind to the offending field; wordings outside AT/DE/LI/LU require your own SepaMandateWording and legal review. (6) billing:doctor now returns failure for missing Stripe event types — add it to CI and fix endpoint subscriptions before deploying, or the deploy gate will (correctly) block you.
Risk framing. Pre-1.0 with recurring minor-version breakage is the main cost; pin versions and budget an upgrade slot per release. The advance-collection default is the change that can quietly alter revenue timing, and grandfathering means the blast radius is new subscriptions only — precisely the ones least represented in staging data. Provider-side gaps remain documented constraints: Stripe method listings lack amounts, countries, and logos, and Mollie's terminal cancel is device-bound. The new regulatory surface (mandate wording per country, § 312k declaration, withdrawal refunds) is not a code-review item; schedule counsel alongside the engineering ticket. Confirm all of this against the near-term roadmap before committing.
How can I help you explore Laravel packages today?