PHP code example of graystackit / laravel-mollie-billing
1. Go to this page and download the library: Download graystackit/laravel-mollie-billing library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
graystackit / laravel-mollie-billing example snippets
'billable_model' => \App\Models\Organization::class,
'billable_key_type' => 'uuid', // 'uuid' | 'ulid' | 'int'
'user_key_type' => 'int', // 'uuid' | 'ulid' | 'int' — primary key type of your auth user model
namespace App\Models;
use GraystackIT\MollieBilling\Concerns\HasBilling;
use GraystackIT\MollieBilling\Contracts\Billable;
use Illuminate\Database\Eloquent\Model;
class Organization extends Model implements Billable
{
use HasBilling;
public function getUsedBillingSeats(): int
{
return $this->users()->count();
}
}
use GraystackIT\MollieBilling\Facades\MollieBilling;
// routes/web.php
// Customer portal — needs auth + your tenant resolution middleware
Route::middleware(['web', 'auth', 'tenant'])->group(function () {
MollieBilling::routes();
});
// Checkout — needs auth but NOT tenant resolution (the checkout creates the tenant)
Route::middleware(['web', 'auth'])->group(function () {
MollieBilling::checkoutRoutes();
});
// Admin panel — auth only, no tenant scope. AuthorizeBillingAdmin runs inside the group.
Route::middleware(['web', 'auth'])->group(function () {
MollieBilling::adminRoutes();
});
// routes/api.php — or any group WITHOUT the `web` middleware
// Mollie webhook. REQUIRED: every payment the package creates carries this URL, so
// without it the very first checkout throws RouteNotFoundException [billing.webhook]
// before it ever reaches Mollie. Mollie calls it server-to-server — no session, no
// CSRF token, no tenant slug — so it must not sit inside the `web` group.
MollieBilling::webhookRoutes();
Route::middleware(['auth', 'tenant'])
->prefix('{organization:slug}')
->group(function () {
MollieBilling::routes();
});
// Checkout lives outside the tenant prefix — the billable is created during checkout
Route::middleware(['auth'])->group(function () {
MollieBilling::checkoutRoutes();
});
use GraystackIT\MollieBilling\Contracts\Billable;
use GraystackIT\MollieBilling\Facades\MollieBilling;
MollieBilling::urlParametersUsing(
fn (?Billable $billable) => $billable
? ['organization' => $billable->slug]
: []
);
use GraystackIT\MollieBilling\Contracts\Billable;
use GraystackIT\MollieBilling\Facades\MollieBilling;
use Illuminate\Http\Request;
MollieBilling::resolveBillableUsing(fn () => auth()->user()?->currentOrganization);
// The second argument is the point: this callback is asked "may THIS requester act on THIS
// billable", and every cross-tenant defence in the package delegates to it — the portal
// middleware, the invoice download, the checkout's prefill gate. A closure that ignores
// `$billable` and only checks `auth()->check()` answers yes for every tenant, which turns the
// checkout route's query-parameter fallback (documented below) into a read of any tenant's
// billing data by route key. Compare the billable against the requester.
MollieBilling::authUsing(
fn (Request $request, Billable $billable): bool => $request->user()?->belongsToBillable($billable) ?? false,
);
use GraystackIT\MollieBilling\Facades\MollieBilling;
// How to create a billable (Organization, Team, …) from checkout form data:
MollieBilling::createBillableUsing(function (array $data) {
return Organization::create([
'name' => $data['name'],
'billing_street' => $data['billing_street'],
'billing_city' => $data['billing_city'],
'billing_postal_code' => $data['billing_postal_code'],
'billing_country' => $data['billing_country'],
'vat_number' => $data['vat_number'],
]);
});
// Optional: run logic before the Mollie payment is created.
// Return null to proceed, or a string to block checkout with that error message.
MollieBilling::beforeCheckoutUsing(function (Billable $billable): ?string {
// e.g. create a User and attach to the billable
return null;
});
// Optional: run cleanup after checkout succeeds or fails.
MollieBilling::afterCheckoutUsing(function (Billable $billable, bool $success): void {
if (! $success) {
// e.g. delete the orphaned user
}
});
// Optional: cascade-delete logic for billables abandoned mid-checkout. The
// CleanupOrphanedBillablesJob runs every 15 minutes and identifies billables
// that never reached an active subscription. When a closure is registered it
// receives the billable and is responsible for cascading cleanup (e.g.
// deleting tenants, users with no other organizations, etc.). Without a
// closure the package falls back to `$billable->delete()`.
//
// The closure may return `false` to veto cleanup for billables that
// legitimately exist without a subscription (admins, employees, internal
// accounts). The job then suppresses ALL side-effects — no CheckoutAbandoned
// event, no mandate revocation, no log entry. Returning `true` or `void`
// behaves like before.
MollieBilling::cleanupOrphanedBillableUsing(function (Billable $billable): bool {
if ($billable instanceof User && ($billable->isAdmin() || $billable->isEmployee())) {
return false;
}
DB::transaction(function () use ($billable): void {
foreach ($billable->users()->get() as $user) {
if ($user->organizations()->where('id', '!=', $billable->id)->doesntExist()) {
$user->forceDelete();
}
}
$billable->forceDelete();
});
return true;
});
use Livewire\Component;
use GraystackIT\MollieBilling\Facades\MollieBilling;
// AppServiceProvider::boot()
MollieBilling::checkoutStepsUsing(fn () => [
[
'key' => 'account',
'label' => 'Account',
'headline' => 'Create your account',
'description' => 'Set up your login credentials before we continue.',
'view' => 'checkout.steps.account', // your app's Blade view
'validate' => function (Component $component) {
$component->validate([
'customData.name' => ['
'enter' => function (Component $component): void {
// Whatever was confirmed before the customer navigated away is no longer current.
$component->customData['email_confirmed'] = false;
$component->customData['confirmation_sent'] = false;
},
class Organization extends Model implements Billable
{
use HasBilling;
public function getUsedBillingSeats(): int
{
return $this->users()->count();
}
}
class User extends Authenticatable implements Billable
{
use HasBilling;
// Option A — point both accessor and mutator at a different column:
protected function billingNameAttribute(): string
{
return 'practice_name';
}
// Option B — full control, e.g. compute or fall back:
public function getBillingName(): string
{
return $this->practice_name ?? '';
}
public function setBillingName(string $name): void
{
$this->practice_name = $name;
}
}
use GraystackIT\MollieBilling\Concerns\HasBilling;
use GraystackIT\MollieBilling\Contracts\Billable;
use Illuminate\Database\Eloquent\Builder;
class Practice extends Model implements Billable
{
use HasBilling;
public function scopeBillableSearch(Builder $query, string $term): Builder
{
return $query->where(function ($q) use ($term) {
$q->where('practice_name', 'like', '%'.$term.'%')
->orWhereHas('owner', fn ($o) => $o->where('email', 'like', '%'.$term.'%'));
});
}
public function scopeBillableOrderByName(Builder $query, string $direction): Builder
{
return $query->orderBy('practice_name', $direction);
}
public function scopeBillableOrderByEmail(Builder $query, string $direction): Builder
{
return $query->leftJoin('users', 'users.id', '=', 'practices.owner_id')
->orderBy('users.email', $direction)
->select('practices.*');
}
}
use GraystackIT\MollieBilling\Concerns\HasBilling;
use GraystackIT\MollieBilling\Contracts\Billable;
use Illuminate\Database\Eloquent\Builder;
class User extends Authenticatable implements Billable
{
use HasBilling;
public function applyBillingScope(Builder $query): void
{
$query->where('is_customer', true);
}
}
use GraystackIT\MollieBilling\Facades\MollieBilling;
// Full plan access for 90 days, no payment method thly',
days: 90,
);
// Addon-only grant — the customer keeps their existing plan:
MollieBilling::coupons()->addonGrantCoupon(
code: 'PRIORITY30',
addonCode: 'priority_support',
days: 30,
);
use GraystackIT\MollieBilling\Facades\MollieBilling;
// Refund a full invoice and issue a credit note:
MollieBilling::refunds()->refundFully($invoice, RefundReasonCode::BillingError);
// Partial refund of a specific net amount (in cents):
MollieBilling::refunds()->refundPartially($invoice, 500, RefundReasonCode::Goodwill, 'customer request');
// Refund specific overage units (auto-calculates amount from unit price, credits wallet):
MollieBilling::refunds()->refundOverageUnits($invoice, 'tokens', 1_000, RefundReasonCode::Goodwill);
// Wallet-only credit without touching Mollie — use WalletUsageService directly:
app(WalletUsageService::class)->credit($organization, 'tokens', 500, 'goodwill bonus');
namespace App\Models;
use GraystackIT\MollieBilling\Contracts\AuthorizesBillingAdmin;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements AuthorizesBillingAdmin
{
public function canAccessBillingAdmin(): bool
{
return $this->is_admin === true;
}
}
use GraystackIT\MollieBilling\Facades\MollieBilling;
use GraystackIT\MollieBilling\Testing\TestBillable;
use GraystackIT\MollieBilling\Testing\BillableStateHelper;
$fake = MollieBilling::fake();
$billable = TestBillable::factory()->create();
BillableStateHelper::onPaidPlan($billable, 'pro', 'monthly');
$billable->recordBillingUsage('api_calls', 1_500);
$fake->assertSubscriptionCreated($billable);