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;
});

<a href="{{ MollieBilling::checkoutUrl('/pricing') }}">Subscribe now</a>

<a href="{{ MollieBilling::checkoutUrl('/pricing', plan: 'pro', interval: 'yearly') }}">
    Get Pro yearly
</a>

// config/mollie-billing.php
'checkout_countries' => [
    'regions' => ['EU'],          // built-in: 'EU' (27 member states)
    ':
'additional_countries' => [
    'CH' => ['vat_rate' => 8.1, 'name' => 'Switzerland'],
],

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;
},

MollieBilling::createBillableUsing(function (array $data) {
    $user = User::create([
        'name'     => $data['custom']['name'],
        'email'    => $data['custom']['email'],
        'password' => Hash::make($data['custom']['password']),
    ]);

    $org = 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'],
    ]);

    $user->organizations()->attach($org);

    return $org;
});

// config/mollie-billing.php
'header_components' => ['pages::components.language-switcher'],



return [
    'plans' => [
        'pro' => [
            'name' => 'Pro',
            'tier' => 2,
            '       'intervals' => [
                'monthly' => [
                    'base_price_net' => 2900,
                    'seat_price_net' => 990,
                    'trial_days' => 14, // optional, per-interval trial length
                    // Included quota per billing period (here: per month). A trial is
                    // credited its prorated share of this — 14 of 30 days = 47 of 100 —
                    // and `grantedBillingQuota()` is what the meters measure against.
                    '   'addons' => [
        'softdrinks' => [
            'name' => 'Softdrinks',
            'feature_keys' => ['softdrinks'],
            'intervals' => [
                'monthly' => ['price_net' => 490],
                'yearly' => ['price_net' => 4900],
            ],
        ],
    ],
];

'supplier' => [
    'name'   => 'Supplier',
    'hidden' => true,
    'tier'   => 3,
    // …
],

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);
    }
}

User::withoutGlobalScope(\GraystackIT\MollieBilling\Scopes\BillingScope::class)->find($id);

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;

MollieBilling::subscriptions()->update($organization, [
    'plan_code' => 'pro',
    'interval' => 'yearly',
    'addons' => ['priority_support' => true],
    'seats' => 12,
    'apply' => 'immediate', // or 'end_of_period'
]);

use GraystackIT\MollieBilling\Services\Billing\UpgradeLocalToMollie;

['checkout_url' => $url, 'payment_id' => $id] = app(UpgradeLocalToMollie::class)->handle($organization, [
    'plan_code'   => 'pro',
    'interval'    => 'monthly',
    'addon_codes' => [],
    'extra_seats' => 0,
    'amount_gross' => $previewedGross, // pre-computed by PreviewService
]);

return redirect()->away($url);

$preview = MollieBilling::preview()->previewUpdate($organization, [
    'plan_code' => 'pro',
    'interval' => 'yearly',
]);

// $preview->prorataCredit, $preview->newChargeGross, $preview->vatAmount, ...

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);
bash
php artisan vendor:publish --tag=mollie-billing-config       # mollie-billing.php + mollie-billing-plans.php
php artisan vendor:publish --tag=mollie-billing-migrations
php artisan vendor:publish --tag=mollie-billing-views        # optional: override Blade views (see the caveat under Customization)
php artisan vendor:publish --tag=billing-lang                # optional: override translations
bash
php artisan migrate
bash
php artisan billing:check-config
bash
php artisan vendor:publish --tag=billing-lang
bash
php artisan vendor:publish --tag=mollie-billing-views
css
@source "../../vendor/graystackit/laravel-mollie-billing/resources/views/**/*.blade.php";
js
content: [
    // ...
    './vendor/graystackit/laravel-mollie-billing/resources/views/**/*.blade.php',
],