Download the PHP package graystackit/laravel-mollie-billing without Composer
On this page you can find all versions of the php package graystackit/laravel-mollie-billing. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download graystackit/laravel-mollie-billing
More information about graystackit/laravel-mollie-billing
Files in graystackit/laravel-mollie-billing
Package laravel-mollie-billing
Short Description Batteries-included Mollie billing for Laravel: VAT/OSS compliance, wallet-based metered billing, coupons, scheduled plan changes, trial flow, admin panel and a Livewire 4 customer portal.
License MIT
Informations about the package laravel-mollie-billing
graystackit/laravel-mollie-billing
Batteries-included Mollie billing for Laravel: VAT/OSS compliance, wallet-based metered billing, coupons, scheduled plan changes, trial flow, admin panel and a Livewire 4 customer portal.
A batteries-included Mollie billing layer for Laravel that wraps mollie/laravel-mollie ^4 and adds VAT/OSS compliance, wallet-based metered billing, a coupon engine, scheduled plan changes, an admin panel, and a Livewire 4 customer portal — all keyed off a Billable contract that lives on whichever model owns the subscription (typically your Organization, not your User).
Highlights
- Mollie subscriptions, mandates and webhooks (built on Mollie's official Laravel SDK v4 with typed request objects)
- VAT calculation, VIES validation and OSS export (
mpociot/vat-calculator) - Country-mismatch reconciliation: three-way reconciliation of user-declared, payment-derived, and IP-derived country at every recurring payment. If the user country matches none of the other signals, the subscription is set to cancel-at-period-end, the billable is notified by email, and the user can self-correct via a dashboard modal (refund + reissue at the corrected VAT rate). B2B billables with a VIES-validated VAT number bypass the check (reverse-charge makes the bank country fiscally irrelevant). Manual admin override available. See docs/vat-handling.md.
- Wallet-based metered billing with included quotas and overage prices (
bavix/laravel-wallet), with case-insensitive usage-type lookups - Direct overage charging with retry and
past_duestate - Five coupon types —
SinglePayment,Recurring,Credits,TrialExtension,AccessGrant - Access Grants for full-plan or addon-only complimentary access
- Scheduled plan changes, prorata, end-of-period downgrades
- Refunds and credit notes (full, overage units, wallet-only)
- IP-based country pre-fill in the checkout/billing-data dropdown (UX only — never persisted)
- Trial flow with Local-to-Mollie subscription conversion
- Feature gating via
@planFeatureBlade directive andbilling.featuremiddleware - Built-in first-checkout flow with configurable country list, VAT/VIES validation and coupon support
- Livewire 4 SFC customer portal and admin panel, both built on Flux Pro
- Promotion links via signed
/promotion/{token}URLs - Localized notifications (English and German out of the box)
- All Livewire SFC views publishable and overridable
Requirements
- PHP 8.3+
- Laravel 12 or 13
- A Mollie account with API key
- Livewire 4 (for the customer portal views)
livewire/flux-pro— required for the customer portal, the checkout flow and the admin panel. Not pulled in by this package (commercial license needed); install it separately in your application:composer require livewire/flux-pro.
Installation
Publish the config and migrations:
Edit config/mollie-billing.php and set the billable model:
Important:
billable_key_typeanduser_key_typemust be set before running migrations for the first time.billable_key_typecontrols the column type of every polymorphic foreign key that references your billable — includingbavix/laravel-wallet'swallets.holder_id,transactions.payable_id, andtransfers.{from,to}_id, which we rewrite from the defaultbiginttouuid/ulid.user_key_typecontrols columns that reference the auth user (e.g.billing_country_mismatches.resolved_by_user_id). Changing them later requires manually altering those columns.
Then run migrations:
Frontend assets
The package ships server-rendered Blade/Livewire views but no compiled CSS or JS — it relies on your host app's Vite pipeline. The shipped layouts (portal, checkout, admin) call @vite(['resources/css/app.css', 'resources/js/app.js']), so your app must:
- Expose those two entry points in
vite.config.js(the Laravel default). - Scan the package's Blade views in your Tailwind build, otherwise utility classes used inside the package will be purged and the portal/checkout will render unstyled. See Tailwind CSS content source below for the one-line
@source(Tailwind v4) orcontent(v3) entry to add.
If you publish the views (--tag=mollie-billing-views) and Tailwind already scans resources/views/**, the published copies are picked up automatically — but the @source line for the vendor path is still needed for any unpublished views.
Verify your configuration before deploying — the package ships a validator that checks both mollie-billing.php and mollie-billing-plans.php for syntax errors, broken references (unknown feature_keys, allowed_addons, product group, …) and likely misconfigurations:
See the Commands section below for the full list of issues it detects.
Quick start
Add the HasBilling trait and implement the Billable contract on your billable model — typically a tenant or organization, not the User:
Configure your environment:
BILLING_MOLLIE_KEY is an alias for MOLLIE_KEY from mollie/laravel-mollie —
either works. The package propagates BILLING_MOLLIE_KEY into mollie.key so
your .env stays on the BILLING_* prefix.
Mount the package routes in routes/web.php. The three route groups serve different scopes and need different middleware:
The admin routes are auto-loaded by the service provider as well, so you only need to call adminRoutes() if you want them under a custom middleware stack.
Multi-tenant URL prefixes
If your app nests the portal behind a tenant parameter (e.g. prefix('{organization:slug}')), mount MollieBilling::routes() inside that group — do not apply your own ->name('tenant.') prefix around it, because the package's views call route('billing.*') by those exact names. Keep checkoutRoutes() outside the tenant group since no tenant exists yet at checkout time:
The package ships a PropagateRouteDefaults middleware that copies the active route's parameters into URL::defaults, so generated links inside the portal (e.g. route('billing.plan')) automatically carry the tenant slug — no app-side URL::defaults wiring required.
For contexts without an active HTTP request — queued notifications, background jobs, or services that run before tenant resolution — PropagateRouteDefaults cannot help. Register a global URL parameter resolver so the package can build correct URLs from any context:
The $billable parameter is null in rare cases where no billable is available yet (e.g. some middleware checks). In those cases the closure should return [] or derive a fallback from auth()->user() or session state.
How the two mechanisms interact:
PropagateRouteDefaultscovers the request context (portal views, form submissions).urlParametersUsingcovers everything else (Mollie webhook URLs, redirect URLs sent to Mollie, queued mail, background jobs). They complement each other — both can be active simultaneously without conflict.
If your billable model needs custom logic beyond the global resolver, you can still override urlRouteParameters() on the model directly — the override takes precedence.
Tell the facade how to resolve the current billable for the authenticated user — usually in AppServiceProvider::boot():
Checkout-route fallback: the checkout route is mounted outside any tenant prefix, so a typical
resolveBillableUsingclosure that reads from a tenant context returnsnullthere. When that happens the package falls back to looking up the billable from the request's query parameters, matching against the billable model'sgetRouteKeyName(). A returning customer hitting/billing/checkout?organization=acme-corptherefore re-uses the existing billable instead of being asked for the company details again. Reserved query keys (back,plan,interval,redirect,token) are skipped.
Customize config/mollie-billing-plans.php to define your plans, addons and feature keys.
First checkout
The package ships a complete first-checkout flow — a multi-step Livewire wizard that collects billing details, lets the customer choose a plan, optional addons/seats, apply a coupon, and redirects to Mollie for payment.
Setup
Register three callbacks in your AppServiceProvider::boot():
Link to checkout
The optional $backUrl parameter controls where the "Back" link in the checkout header leads. When omitted, the package falls back to config('mollie-billing.checkout_back_url') (default /).
To pre-select a plan and/or billing interval, pass them as additional parameters:
The plan step will still be shown so the customer can change their mind, but the given plan will be pre-selected. Invalid plan codes or intervals are silently ignored.
Checkout countries
By default the checkout shows all 27 EU member states. Customize via config:
Country names are translated via the package's billing::countries lang files (English and German included). Publish and extend them for additional locales:
Custom checkout steps
If your app needs additional steps before the billing-address form (e.g. "Create your account"), register them via the facade. Custom steps are inserted before the package's built-in steps; numbering, timeline and navigation adjust automatically.
Each step definition requires:
| Key | Type | Description |
|---|---|---|
key |
string |
Unique identifier for the step. |
label |
string |
Short label shown in the timeline. |
headline |
string |
Heading displayed above the step content. |
description |
string |
Subheading text below the headline. |
view |
string |
Blade view name to @include for this step's form fields. |
validate |
Closure |
(optional) Receives the Livewire Component instance. Throw a ValidationException (or call $component->validate(...)) to block navigation. |
Binding form data — The checkout component exposes a public array $customData = [] property. Use wire:model with dot notation in your step view:
The customData array is passed to your createBillableUsing callback as $data['custom'], so you can access it when creating the billable:
You can register multiple custom steps — they appear in the order returned by the callback.
Customizing views
All Livewire views (checkout, portal, admin) can be published and customized:
Views are published to resources/views/vendor/mollie-billing/. SFC files use the ⚡ prefix convention (e.g. ⚡checkout.blade.php).
Tailwind CSS content source
The package's Blade views use Tailwind utility classes (including responsive breakpoints like sm:, lg:). Your host app's Tailwind build must scan the package views, otherwise these classes will be purged.
Tailwind v4 — add a @source directive in your resources/css/app.css:
Tailwind v3 — add the path to the content array in tailwind.config.js:
Without this, responsive grid layouts and other utility classes in the portal, checkout and admin panel may not render correctly.
Configuration
Highlights of config/mollie-billing.php:
| Key | Purpose |
|---|---|
currency |
Default currency for prices and invoices (e.g. EUR). |
logo_url |
Logo displayed in checkout and portal headers. |
primary_color |
Accent color for checkout UI (hex, e.g. #6366f1). |
dashboard_url |
URL the portal logo links to (e.g. your app's main dashboard). Supports route: prefix. |
checkout_back_url |
Where the checkout "Back" link leads (default /). |
checkout_countries |
Countries shown in checkout (regions, include, exclude). |
allow_overage_default |
Default policy when a plan does not declare its own overage rule. |
additional_countries |
ISO-3166 codes + VAT rates for non-EU jurisdictions. |
vat_rate_overrides |
Map of country code to override VAT percentage. |
company_name |
Display name used in headers, notifications and signatures. |
billable_model |
Fully-qualified class name of your billable model. |
billable_key_type |
uuid, ulid, or int — determines morph column shape. |
user_key_type |
uuid, ulid, or int — primary key type of your auth user model (used e.g. for billing_country_mismatches.resolved_by_user_id). |
billing_timezone |
IANA timezone for the customer portal display (BILLING_TIMEZONE, default UTC). Persistence and computation always remain UTC; the admin panel renders UTC. See Timezones. |
Portal "back to dashboard" link
By default the portal logo links to the billing dashboard itself. Set dashboard_url to link it to your app's main dashboard instead — a "Back to dashboard" link will also appear at the bottom of the sidebar:
Plans and addons
Define your catalog in config/mollie-billing-plans.php. Free plans run as SubscriptionSource::Local (no Mollie subscription), paid plans are SubscriptionSource::Mollie.
The Billable contract
A minimal billable model needs the trait plus one required method — getUsedBillingSeats(). This method is intentionally not provided by the trait because only your app knows how to count active seats (team members, users, etc.):
The seat count is used during plan-change previews to calculate whether extra seats need to be purchased on the new plan.
Building on getUsedBillingSeats(), the trait derives seat availability for you:
getAvailableBillingSeats(): int— configured seat count minus seats in use (never negative).isBillingSeatAvailable(int $count = 1): bool— whether$countmore seats can be assigned without exceeding the seat count. A non-positive$countis treated as a request for one seat.
HasBilling provides among others:
recordBillingUsage($type, $quantity)andcreditBillingUsage(...)hasPlanFeature('reports.export')cancelBillingSubscription(),changeBillingPlan(...),enableBillingAddon(...)billingPortalUrl(),billingPlanChangeUrl()latestBillingInvoice()andbillingInvoices()morph relationgetWallet($type)/hasWallet($type)/createWallet($data)— overridden bavix wrappers that resolve usage-type slugs case-insensitively (sotokens,Tokens, andTOKENSall hit the same wallet, andcreateWalletwill not insert a duplicate row when a casing variant already exists). Catalog lookups (includedUsage,usageOveragePrice) follow the same case-insensitive rule. See Usage Billing — Casing of usage-type identifiers.
Billing name vs. personal name
The checkout collects a company name and writes it through the getBillingName() / setBillingName() pair on the billable. By default both methods read and write the model's name column, which is fine when the billable is a dedicated Organization / Tenant model.
When your billable is the User model, however, name typically already stores the user's personal name. To keep the personal name intact while still letting the customer enter a company name for invoices, override the two methods (or billingNameAttribute()) on the billable model:
The name key passed to your createBillableUsing callback always carries the company name from the checkout form — your callback decides which attribute to persist it into.
The admin panel never reads the name / email columns directly — every billable label is rendered through getBillingName() / getBillingEmail(), so overriding the accessors above is enough to make the admin listings, the billable detail header, scheduled-changes, past-due, refunds and grant views all show the right value.
Admin search & sort on custom columns
Display goes through getBillingName() / getBillingEmail(), but the admin listings also search and sort by name and email — and a query can't call a PHP accessor. When your display name or contact email lives on a different column (or behind a relation), point the three search/sort scopes at the right place. They default to the name and email columns, so a User-as-billable setup needs no override:
All three are part of the Billable contract, so your IDE/static analysis will flag them if you implement the interface manually instead of using the HasBilling trait.
Restricting which rows are treated as billable
When the billable table also stores rows that are not customers — typically a single users table that mixes admins/staff with paying users — override applyBillingScope() on the model. The HasBilling trait registers BillingScope as a global scope and delegates to this method, so the same filter applies to admin listings, KPI queries, and every lifecycle job that iterates billables.
Bypass the scope on the rare lookups that must reach every row regardless of the app-defined restriction (Mollie webhook resolution, retry jobs, admin impersonation):
The default implementation is a no-op, so models where the table only contains billables need not override it.
Coupon types
| Type | Behavior | Quick example |
|---|---|---|
SinglePayment |
Discounts only a single invoice (Subscription Checkout or One-Time-Order). 100 % is supported — Subscription Checkout uses a Mandate-Only flow so Mollie keeps a mandate for period 2; One-Time-Order skips Mollie entirely and writes a local 0-EUR audit invoice. | MollieBilling::coupons()->singlePaymentCoupon('LAUNCH', 50, 'percent'); |
Recurring |
Discounts each invoice for N periods (Subscription Checkout or any plan-change-style flow). 100 % is supported via a deferred Mollie startDate. Not accepted on One-Time-Orders (no follow-up charges to attach to). |
MollieBilling::coupons()->recurringCoupon('LOYAL', 10, 'percent', periods: 6); |
Credits |
Adds wallet credit balance. | MollieBilling::coupons()->creditsCoupon('PROMO5', cents: 500); |
TrialExtension |
Extends the active trial. | MollieBilling::coupons()->trialExtensionCoupon('EXTEND14', days: 14); |
AccessGrant |
Grants free access without payment. | See below. |
Access Grants
Access Grants come in two flavors — full-plan grants and addon-only grants:
Updating subscriptions
The update orchestrator handles plan changes, addon toggles and seat sync atomically:
Local subscriptions
The package distinguishes two subscription sources via the subscription_source column:
mollie— a real Mollie subscription with mandate, recurring charges and invoices.local— a free / coupon-granted subscription with no Mollie mandate. The wallet receives the included usages on activation (and at scheduled renewals viaPrepareUsageOverageJob), but no money flows.
When does a Local subscription arise?
| Trigger | Service | Notes |
|---|---|---|
| Free plan checkout | StartSubscriptionCheckout → ActivateLocalSubscription |
Mollie returns no checkout_url for a 0 € first payment; the app activates the plan locally. |
AccessGrant coupon |
CouponService::applyAccessGrant → ActivateLocalSubscription |
Coupon-granted plans (timed or unlimited) live as Local. |
| Mollie → Free downgrade | UpdateSubscription |
Cancels the Mollie subscription, sets subscription_source = local, status remains active, wallets are rebalanced (purchased credits preserved). |
What is allowed on a Local subscription?
A Local subscription has no Mollie mandate, so no money can flow from the customer. Anything that would result in a charge is blocked.
| Operation | Allowed? |
|---|---|
| Free addons (price 0) | yes |
| Paid addons | no — LocalSubscriptionDoesNotSupportPaidExtrasException |
Extra seats on a plan with seat_price_net > 0 |
no — same exception |
| Switch to another free plan | yes |
Switch directly to a paid plan via UpdateSubscription |
no — LocalSubscriptionUpgradeRequiresMolliePathException. Use UpgradeLocalToMollie instead (the bundled plan-change UI does this automatically). |
Track metered usage (recordBillingUsage) |
yes — included quota is credited and reset at period boundaries |
| Charge the customer for usage overages | no — PrepareUsageOverageJob only charges Mollie + mandate billables. Negative balances on Local subs are silently reset at the next period. See docs/usage-billing.md. |
| Purchase one-time products | opt-in via config('mollie-billing.local_subscription.allow_one_time_orders'). Default is false — purchase attempts throw LocalSubscriptionCannotPurchaseProductsException and the products page hides the buy buttons. Set to true if your business model treats the free plan as a default tier monetised through token packs etc. |
| Cancel | yes — status switches to cancelled, wallets are kept until subscription_ends_at. |
If you need to bill free-tier users for usage overages or sell them seat upgrades, do not ship the plan at price 0. Set the lowest non-zero amount that still makes commercial sense — that triggers the regular Mollie checkout, captures a mandate, and makes the user a paid (Mollie-source) subscriber.
Local → Mollie upgrade
The webhook on the resulting first payment carries metadata.upgrade_from_local = true and routes through MollieWebhookController::handleLocalToMollieUpgrade(), which reuses the existing wallet (purchased balance preserved) instead of seeding a fresh one.
The bundled plan-change UI (resources/views/livewire/billing/⚡plan-change.blade.php) detects Local → paid plan automatically and shows a confirmation step before the Mollie redirect — no second checkout wizard.
Mollie → Free behaviour
A user-initiated downgrade follows whatever config('mollie-billing.plan_change_mode') is set to (Immediate, EndOfPeriod, UserChoice). For EndOfPeriod, ScheduleSubscriptionChange queues the change and PrepareUsageOverageJob applies it at the period boundary — the same Mollie cancel + Source=Local flip happens then.
purchased_balance (one-time orders, coupon credits) is preserved across every plan change, including downgrades to free.
Preview
Preview the financial impact of an update before applying it:
Refunds
Three convenience methods cover the common cases:
Admin panel
The admin panel lives at /billing/admin. Authorize access by implementing AuthorizesBillingAdmin directly on your user model. The billing.admin middleware checks auth()->user() instanceof AuthorizesBillingAdmin && canAccessBillingAdmin(); users without the interface receive a 403.
Promotion links
Generate signed promotion URLs that auto-apply a coupon when the customer follows them:
Tokens are generated via MollieBilling::coupons()->promotionToken($coupon).
Events
Every state change dispatches a Laravel event so apps can react via listeners. Notable events include:
CheckoutStarted,CheckoutAbandonedSubscriptionCreated,SubscriptionCancelled,SubscriptionExpired,SubscriptionResumedPlanChanged,SubscriptionUpdated,SubscriptionChangeScheduledTrialStarted,TrialConverted,TrialExpired,TrialExtendedMandateUpdatedPaymentSucceeded,PaymentFailed,PaymentAmountMismatch,DuplicatePaymentReceivedInvoiceCreated,InvoiceRefunded,CreditNoteIssued,InvoicePdfRegeneratedOverageCharged,OverageChargeFailedCouponRedeemed,GrantRevokedCountryMismatchFlagged,CountryMismatchResolvedWalletCredited,UsageLimitReached
Subscribe in your EventServiceProvider exactly like any other Laravel event.
Testing
The package ships with helpers for both unit and feature tests:
Commands
See docs/testing-flows.md for the full list of simulated flows, options, and what is/isn't covered.
billing:check-config reports two classes of issues:
- Errors — broken references or invalid values that will cause runtime failures (missing
billable_model, planfeature_keyspointing at undefined features,invoices.disknot declared inconfig/filesystems.php, invalid serial-number format, unknownplan_change_mode, …). Exits with status1. - Warnings — likely misconfigurations that don't break the app but degrade behavior (incomplete invoice seller data,
included_usagesquota without a matchingusage_overage_pricesentry, features defined but never referenced, ambiguous tier ranking, …). Exit status stays0.
Run it after editing either config file or as part of CI to catch typos before deployment.
Documentation
Detailed technical documentation is available in the docs/ directory:
- Configuration —
mollie-billing.phpandmollie-billing-plans.phpreference - Plan Changes — deferred upgrade flow, validation rules, events, extension points
- Subscription Lifecycle — states, transitions, service overview
- Lifecycle and Cleanup — orphaned checkouts, billable deletion cascade, past-due auto-cancel, paid-without-billable reconciliation, mandate policy
- VAT Handling — VAT calculation, VIES, OSS, country reconciliation, automatic resolution
- Notifications — recipient resolution, translation overrides, swapping notification classes with
useNotification() - Timezones — UTC persistence and computation, per-user portal timezone, UTC-rendered admin views
- Testing Lifecycle Flows —
billing:simulateandbilling:webhook-replayfor reproducing every lifecycle transition on staging
Architecture
This package wraps mollie/laravel-mollie ^4 and adds a VAT/OSS layer (mpociot/vat-calculator plus VIES), a wallet layer for metered billing (bavix/laravel-wallet), a coupon engine, a built-in first-checkout wizard, an admin panel and a Livewire 4 customer portal. Subscription lifecycle is split into single-purpose service classes per action (Start, Create, Activate, Change, Cancel, Resubscribe, EnableAddon, DisableAddon, SyncSeats) — the HasBilling trait delegates to them via the container, so apps customize behavior by rebinding services rather than subclassing models. Extension points are provided via facade callbacks (createBillableUsing, beforeCheckoutUsing, afterCheckoutUsing, resolveBillableUsing, etc.) and events.
Webhook handling is similarly split: MollieWebhookController is a thin façade that reserves the payment, fetches it from Mollie, and routes by payment type to a dedicated handler in src/Services/Webhook/ (FirstPaymentHandler, MandateOnlyPaymentHandler, SubscriptionPaymentHandler, ProrataChargeHandler, SingleChargeHandler, CountryCorrectionHandler, LocalToMollieUpgradeHandler, OneTimeOrderHandler, RefundHandler). Each handler is auto-resolved from the container — apps can rebind any of them to customize per-payment-type behavior without touching the controller.
Free or zero-price plans run as SubscriptionSource::Local without a Mollie subscription; paid plans are SubscriptionSource::Mollie. Paid plans with a configured trial_days go through a Mandate-Only checkout (0 EUR, captures the payment method) and create the Mollie subscription with startDate = now + trial_days — no charge during the trial, status = trial, wallet hydrated aliquot to the trial length. See Subscription Lifecycle.
License
The MIT License (MIT). See LICENSE for details.
Credits
All versions of laravel-mollie-billing with dependencies
brick/money Version ^0.11
bavix/laravel-wallet Version ^11.5|^12.0
elegantly/laravel-invoices Version ^4.8
dragonmantank/cron-expression Version ^3.4
laravel/framework Version ^12.0|^13.0
laravel/prompts Version ^0.3|^1.0
livewire/flux Version ^2.0
livewire/livewire Version ^4.0
mollie/laravel-mollie Version ^4.1
mpociot/vat-calculator Version ^3.26
spatie/laravel-activitylog Version ^4.12|^5.0
symfony/http-foundation Version ^7.0|^8.0