Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Billing Laravel Package

ptuchik/billing

ptuchik/billing is a Laravel package for adding billing and subscription management to your app. It provides models and traits for plans, subscriptions, invoices, payments, coupons, and transactions, helping you implement recurring billing and track charges with less custom code.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and publish. Pull it in with Composer. Laravel 5.5 auto-discovers the service provider; on a non-standard skeleton (or if discovery is disabled) register BillingServiceProvider in config/app.php. Then publish the config and migrations:

    composer require ptuchik/billing
    php artisan vendor:publish --provider="Ptuchik\Billing\ServiceProvider"
    php artisan migrate
    

    The publish step gives you config/billing.php and the migrations for packages, plans, coupons, addons, subscriptions, and payments.

  2. Make your model billable. Add the package's Billable trait to App\User (or whichever model owns subscriptions/payments). This gives you the relationship methods (subscriptions, payments, addons) and the helper methods (subscribe, unsubscribe, subscribed, subscription, charge) that everything else builds on.

  3. Seed the catalog. Understand the hierarchy before writing any code: a Package is the product container (e.g. "Pro"), a Plan is the purchasable SKU with a price and an interval ("Pro Monthly, 9.99/mo"). Addons hang off plans/subscriptions, Coupons discount a purchase, Payments are the ledger, Subscriptions link a billable model to a plan. Seed one Package with two Plans (monthly/annual) in a seeder and you have enough to exercise the whole flow.

  4. First use case — gate a feature. Subscribe a user, then check status in a route or middleware:

    $user->subscribe($plan, $coupon /* optional */);
    if ($user->subscribed('main')) { /* show the paid feature */ }
    

Where to look first: config/billing.php (currency, grace period, gateway settings), the published migrations (they define the column names you'll query against), src/Models/* for relationship names, and the Billable trait to see the real method signatures — the README covers the happy path but the trait is the source of truth.

Implementation Patterns

  • Subscription lifecycle. Treat subscribe → renew/extend → cancel as explicit transitions. The package stores an end date on the subscription; the typical pattern is to leave the subscription record intact when cancelling and let your own subscribed() gate check the date, so you keep history and can offer reactivation. Wire a scheduled command (Kernel::schedule) to expire/notify at end-of-period rather than doing it lazily on every request.

  • Middleware for gated routes. Wrap paid routes in a group with a middleware that resolves the billable model and calls subscribed($name), redirecting to a checkout page when false. This keeps controllers clean and gives you one place to change gating logic (e.g. allow a grace window).

  • Checkout as a wrapper, not the package's job. This is a library, not an admin panel — you build the checkout page, coupon form, and plan selection UI. The usual shape is: controller validates a Request, resolves the Plan, passes an optional coupon, calls subscribe/charge, and redirects on success. Keep payment provider calls inside a service class so you can swap gateways without touching the controller.

  • Coupons. Validate a coupon (code, expiry, usage limit, applicable plan) before charging, and apply it as a discount on the plan price. Store the coupon reference on the subscription/payment record so you can report on redemptions later.

  • Addons. Model them as optional modifiers on a plan with their own price; attach to a subscription at checkout (or later via an "add addon" endpoint) and price them into the renewal amount. Keep addon pricing prorated to the remaining period if you offer mid-cycle changes.

  • Payments as an append-only ledger. Never update a payment row to represent a new charge — insert a new one with a status (pending/completed/failed/refunded) and a reference to the gateway transaction ID. Reports, invoices, and dunning all read this table.

  • Testing. Use RefreshDatabase plus seeded packages/plans in a base test case, and Carbon::setTestNow() to step through billing periods. Assert on state (subscribed(), end dates, payment rows) rather than mocking the gateway on every test.

Gotchas and Tips

  • Package vs. Plan confusion is the #1 time sink. People try to subscribe to a Package. You subscribe to a Plan. If subscribe() fails or throws, check you passed a Plan instance, not a Package.

  • Currency and precision. Money is usually stored in minor units (cents) or as a decimal column depending on config — verify in the migration before formatting anything to the user, and be consistent across plan prices, coupon discounts, and payment totals. Mixing the two produces off-by-100 errors that are painful to find later.

  • Coupon usage races. If coupons have a usage limit, two concurrent checkouts can both pass the "remaining uses" check. Wrap coupon validation + increment in a transaction with a row lock (lockForUpdate) or a unique constraint on redemption.

  • Gateway webhooks need idempotency. Payment providers retry. Deduplicate on the gateway transaction ID before inserting a Payment or extending a subscription, or you'll double-charge/double-extend.

  • Migrations are published, not loaded. If you update the package, new migrations won't appear automatically — re-publish or diff the vendor migrations and create your own additive migrations. Never blindly re-run vendor:publish over an existing config.

  • Date/timezone handling. Subscription end dates, trials, and grace periods are Carbon instances; set the app timezone consistently and compare with isFuture()/isPast() rather than raw string comparisons. Test around midnight boundaries.

  • Trait collisions. If your User model already has subscriptions() or payments() methods (from Cashier or your own code), the trait will conflict. Rename or alias before you spend an afternoon on a mysterious method-not-found.

  • Config caching. php artisan config:cache after changing config/billing.php in production; the classic "it works locally" bug is an edited config file that the cached config ignores.

  • Extension points worth knowing: the payment gateway is typically a driver/contract you can implement for your own provider; models can be swapped by extending the package models and binding them in a service provider; and the package usually fires events on subscribe/cancel/payment that you can listen to for emails and analytics. When in doubt, grep the src/Contracts and src/Events directories — that's where the seams are.

  • Debugging: log the full request/response payloads from your gateway driver, not just the result, and keep the raw gateway reference on the payment row. When a subscription "didn't activate," the answer is almost always a failed payment row or a mismatched plan ID.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle