LarsaSub is launching soon. Purchases and account registration open shortly.

Documentation for larsasub v0.38.2. On an older version? Your release notes ship with the package (CHANGELOG.md).

Quick start

This page takes you from composer require to a first paying subscriber on a single Mollie account, then shows what changes when a team pays, when you bill through Mollie Connect, and with dual billing. Setup holds the details of each installation step; Configuration has every setting.

Before you start

You need Why
PHP ^8.3, Laravel ^12.11 or ^13.0 The package's own requirements
A Mollie account and API key Every payment runs through Mollie; a test_ key keeps everything in test mode
A public HTTPS URL Mollie reports every payment's outcome to your webhooks. On localhost, use a tunnel (see Configure the environment)
Cron and a queue worker Charges are planned and collected by scheduled commands, and notifications are queued

The full checklist for a live installation is in Before you install.

Install the package

LarsaSub is not on Packagist. composer require resolves only after you have registered the private repository on larsasub.eu and authenticated with your license token; without that, composer reports "package not found". See the installation guide on larsasub.eu/docs.

composer require peters-development/larsasub
php artisan larsasub:install

larsasub:install publishes the configuration and the dashboard assets, runs the migrations (an interactive run asks first, because migrate applies every pending migration in your application) and seeds the payment methods, all of them inactive. It is safe to run again. See Installing the package for its options.

Make a model billable

The model that pays implements Contracts\BillableModel and uses three traits:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use PetersDevelopment\LarsaSub\Contracts\BillableModel;
use PetersDevelopment\LarsaSub\PaymentProviders\Mollie\Traits\IsMollieBillableModel;
use PetersDevelopment\LarsaSub\Traits\HasSubscriptions;
use PetersDevelopment\LarsaSub\Traits\IsBillableModel;

class User extends Authenticatable implements BillableModel
{
    use HasSubscriptions, IsBillableModel, IsMollieBillableModel;

    public function getBillableNameAttribute(): string
    {
        return $this->name;
    }

    public function getBillableEmailAttribute(): string
    {
        return $this->email;
    }

    public function getBillableIdentifierAttribute(): string
    {
        return (string) $this->id;
    }
}
Piece What it does
BillableModel The contract: the three accessors above, and getNotifiableUsers(), which IsBillableModel already implements (a Notifiable model mails itself; otherwise the billable e-mail address is used)
IsBillableModel The billing relations and helpers: credit, single payments, mandates, payableCharges(), outstandingCharges()
IsMollieBillableModel Required with the Mollie provider. The checkout calls getMollieCustomer(), createMollieMandate() and recordExistingMollieMandate() on your model. Without the trait the first customer who tries to pay gets an error naming the missing trait instead of a payment screen; php artisan larsasub:doctor reports it before that happens
HasSubscriptions activeSubscription, hasFeature(), featureValue() and the other subscription shortcuts. Optional; the matching Contracts\HasSubscriptions interface is optional too, for your own type hints

billable.model in config/larsasub.php defaults to App\Models\User. Change it when another model pays.

You never create billing records by hand. LarsaSub links your model to every billing record through a Billable registration (in larsasub_billables), created automatically on the first billing operation. See Billable models.

Configure the environment

# Mollie API key — a test_ key keeps every call in test mode
MOLLIE_KEY=test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Who is an admin: a role name, or e-mail addresses separated by commas.
# Empty means nobody, and plan management and the operator dashboards deny everyone.
LARSASUB_ADMIN_ROLE=admin@yourcompany.com

# Who receives payment-failure and cancellation alerts.
# Empty means those alerts are discarded.
LARSASUB_NOTIFICATIONS_SALES=sales@yourcompany.com

# Defaults, shown for completeness. EUR is the only supported default currency
LARSASUB_PROVIDER=mollie
LARSASUB_CURRENCY_DEFAULT=EUR
LARSASUB_NOTIFICATION_CHANNELS=mail

# Local development only: a public URL for Mollie's webhooks
LARSASUB_NGROK_DOMAIN=https://your-subdomain.ngrok.io

Every notification can be switched off on its own; see Notifications. Invoicing ships off: read Invoicing and VAT before you turn it on.

Activate payment methods

The installer seeds every supported Mollie method inactive, so nobody can pay until you choose what you accept. Activate only methods your Mollie account has enabled:

use PetersDevelopment\LarsaSub\Models\PaymentMethod;

PaymentMethod::query()
    ->where('provider', 'mollie')
    ->whereIn('provider_id', ['directdebit', 'ideal', 'creditcard'])
    ->get()
    ->each(fn (PaymentMethod $method) => $method->update(['active' => true]));

SEPA Direct Debit opens its mandate with a first payment in another method: iDEAL by default (the method's first_payment_method_id). That method must be enabled on your Mollie account, whether or not you activate it here for one-time payments. The admin companions (Nova, Filament) can do the same from a screen. See Payment methods and mandates.

Run the scheduler and a queue worker

The package registers its own scheduled commands. Planning, collecting, renewing and ending subscriptions all happen there, so without cron nothing is ever charged:

# crontab
* * * * * cd /path-to-your-app && php artisan schedule:run >> /dev/null 2>&1

# a long-running worker for the queued notifications and listeners
php artisan queue:work

The sync queue is not supported outside local development. See Scheduled commands.

Subscribe a customer

A customer subscribes to a plan (a Subscription) with a recurring payment method. You need at least one active plan: create it in the management dashboard (/billing/admin/subscriptions), in one of the admin companions, or on the model (The Subscription plan). On a fresh installation, php artisan larsasub:install --seed adds two example plans.

Through the bundled dashboard

Send the signed-in customer to /billing/dashboard (route larsasub.my-subscription.view). There they pick a plan and a payment method, accept your terms when you have configured them, and go to Mollie's checkout. Afterwards Mollie sends them back to the dashboard, and the subscription activates when the webhook reports the first payment as paid. The URL is standalone.url.subscription-dashboard; see Standalone dashboards.

From your own code

A headless host asks the payment provider to create the subscription and start the first payment:

use PetersDevelopment\LarsaSub\Models\PaymentMethod;
use PetersDevelopment\LarsaSub\Models\Subscription;
use PetersDevelopment\LarsaSub\PaymentProviders\PaymentProviderFactory;

$plan = Subscription::query()->isEnabled()->findOrFail($request->input('plan_id'));

$method = PaymentMethod::query()
    ->isEnabled()
    ->isRecurring()
    ->where('provider_id', 'directdebit')
    ->firstOrFail();

return PaymentProviderFactory::make(config('larsasub.provider'))
    ->createSubscription($request->user(), $plan, $method);

createSubscription() returns a RedirectResponse to Mollie's checkout. The subscription stays pending until the webhook reports the first payment as paid; then it activates, and the mandate that payment created collects every later charge. A plan that collects nothing now follows subscription.free_plan_mandate instead of going to a checkout.

Subscribing on a mandate the customer already has, starting after a trial and cost lines on the first charge are in Subscriptions.

Check access

$subscription = $user->activeSubscription;          // ?BillableSubscription

if ($subscription !== null) {
    // the user has a running subscription
}

$user->hasFeature('premium_support');               // bool
$limit = $user->featureValue('api_calls') ?? 100;   // null without an active subscription

$subscription?->paidThrough();                      // ?CarbonImmutable: last day paid for

Read activeSubscription as a property. With parentheses, activeSubscription() is the relation, an object that is always truthy. Whether a customer keeps access when paidThrough() passes is your decision; the package never cuts access itself (Paid through).

Across all subscribers, chain the scopes on a query:

use PetersDevelopment\LarsaSub\Models\BillableSubscription;

BillableSubscription::query()->isActive()->get();
BillableSubscription::query()->isChargeable()->get();

From a signed-in user to the model that pays:

use PetersDevelopment\LarsaSub\Helpers\BillableResolver;

$owner = BillableResolver::resolve($user);           // the user, or its team in indirect mode
$owner = BillableResolver::fromRequest($request);
$isDirect = BillableResolver::isDirect();            // true when the user itself pays

In explicit mode BillableResolver::resolve() throws unless billable.owner_resolver is configured; see Explicit mode.

Billing a team or a company

By default the user pays (direct billing). When a related model pays, so that several users share one subscription, point billable.model at it and name the relation on the user:

// config/larsasub.php
'billable' => [
    'model' => \App\Models\Team::class,
    'relation' => 'team',   // $user->team() returns the team that pays
],

The team takes the contract and the traits the user had above:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use PetersDevelopment\LarsaSub\Contracts\BillableModel;
use PetersDevelopment\LarsaSub\PaymentProviders\Mollie\Traits\IsMollieBillableModel;
use PetersDevelopment\LarsaSub\Traits\HasSubscriptions;
use PetersDevelopment\LarsaSub\Traits\IsBillableModel;

class Team extends Model implements BillableModel
{
    use HasSubscriptions, IsBillableModel, IsMollieBillableModel;

    public function getBillableNameAttribute(): string
    {
        return $this->name;
    }

    public function getBillableEmailAttribute(): string
    {
        return $this->contact_email;
    }

    public function getBillableIdentifierAttribute(): string
    {
        return (string) $this->id;
    }

    // Optional: who receives the team's billing mail
    public function getNotifiableUsers(): Collection
    {
        return $this->users;
    }
}

Feature checks then go through the team: $user->team->hasFeature('premium_support'). When one user acts for several billables, use explicit mode instead; see Billable models.

Mollie Connect

With Mollie Connect every tenant of your platform connects its own Mollie account, and its customers' payments land there, with an optional application fee for you. Connect collects payments on behalf of others, which requires the Platform license tier. Declare it next to the switch: without the declaration the package refuses to boot. Nothing is verified over the network and nothing is sent anywhere; see larsasub.eu/pricing.

LARSASUB_LICENSE_TIER=platform
LARSASUB_CONNECT_ENABLED=true
LARSASUB_CONNECT_TENANT_RELATION=organization
LARSASUB_CONNECT_TENANT_LABEL=organization
LARSASUB_CONNECT_TENANT_NAME_ATTRIBUTE=name
LARSASUB_CONNECT_TENANT_OWNER_ATTRIBUTE=owner_id

# Mollie OAuth app (Mollie Dashboard → Developers → OAuth apps)
MOLLIE_CONNECT_CLIENT_ID=app_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MOLLIE_CONNECT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Your own Mollie organization: no application fee is charged on it
MOLLIE_ORGANIZATION_ID=org_xxxxxxxx

The same settings in config/larsasub.php:

'license' => [
    'tier' => 'platform',                   // required for Connect
],

'billable' => [
    'model' => \App\Models\User::class,     // the tenants' customers pay
],

'connect' => [
    'enabled' => true,
    'tenant_relation' => 'organization',    // $user->organization() returns the tenant
    'tenant_label' => 'organization',       // the word used in the UI and in mails
    'tenant_name_attribute' => 'name',      // $tenant->name is shown in customer mails
    'tenant_owner_attribute' => 'owner_id', // the tenant's owner may manage the Mollie connection
],

php artisan larsasub:license shows the declared tier and whether the enabled features fit it. See Mollie Connect for the scopes, website profiles, disconnecting a tenant and settlements.

Application fees

Charge a platform fee on every tenant payment:

LARSASUB_CONNECT_FEE_ENABLED=true
LARSASUB_CONNECT_FEE_TYPE=percentage    # percentage or fixed
LARSASUB_CONNECT_FEE_VALUE=10           # 10% or €10,00
LARSASUB_CONNECT_FEE_DESCRIPTION="Platform fee"

A plan can override the fee with its fee_type and fee_value fields. No fee is charged when the platform model itself pays, or when the tenant's Mollie organization is your own (connect.platform_organization_id). See Application fees.

The tenant OAuth flow

  1. The tenant owner clicks "Connect Mollie" on the tenant connect dashboard (/billing/admin/connect, standalone.url.tenant-connect-dashboard).
  2. LarsaSub redirects to Mollie's authorization page.
  3. The tenant authorizes your platform on its Mollie account.
  4. Mollie redirects back with an authorization code.
  5. LarsaSub exchanges the code for access and refresh tokens.
  6. The tokens are stored encrypted on the tenant's connected account (larsasub_mollie_connected_accounts).
  7. The tenant can receive payments.

Tenant-scoped plans and customers

In Connect mode Mollie customers and mandates live per tenant, and plans can be the tenant's own or global:

use PetersDevelopment\LarsaSub\Models\Subscription;

// Created under the tenant's Mollie account, with the tenant's access token
$user->getMollieCustomer();

// The tenant's plans plus the global ones
$plans = Subscription::query()
    ->forTenant($tenant, includeGlobal: true)
    ->isEnabled()
    ->get();

Dual billing

When the platform bills its tenants itself and the tenants bill their own customers through Connect, name the tenant model as the platform's billable:

// config/larsasub.php
'billable' => [
    'model' => \App\Models\User::class,                   // tenants' customers: tenant's Connect account
    'platform_model' => \App\Models\Organization::class,  // billed by the platform: your own Mollie key
],

Both models implement BillableModel and use the traits shown above. MollieClientResolver routes each payment to the right account: payments of the platform model on your own API key, those of the users on the tenant's access token. See Dual billing.

The bundled dashboards

Twelve standalone pages ship with the package, each switchable under standalone.dashboards and with its URL under standalone.url:

Default URL For Shows
/billing/dashboard Customer The subscription: subscribe, change, cancel
/billing/payments Customer Payment history; paying a failed charge or an open single payment
/billing/payment-methods Customer Payment methods and mandates
/billing/credit Customer Credit balance and history
/billing/profile Customer Billing details: invoice profile, VAT number, country
/billing/invoices Customer Invoices (with invoicing on)
/billing/admin/subscriptions Management Plans
/billing/admin/subscribers Management Subscribers
/billing/admin/charges Management Charges
/billing/admin/invoices Management Invoices (with invoicing on)
/billing/admin/connect Tenant owner The Mollie connection (Connect mode only)
/billing/admin/fees Tenant owner Application fees (Connect mode only)

In Connect mode the management pages are the tenant owner's view of the tenant; in simple mode they are operator management, granted through larsasub.policies.admin_role. See Standalone dashboards for middleware, the guard and theming.

Commands you will use

# Check configuration, schema and the billable model
php artisan larsasub:doctor

# Show the declared license tier
php artisan larsasub:license

# Two example plans (fresh installation only: skipped on an existing one,
# so it cannot duplicate a live catalogue)
php artisan larsasub:install --seed

# The scheduled work, run by hand while you test
php artisan larsasub:plan-charges
php artisan larsasub:execute-charges
php artisan larsasub:notify-charges
php artisan larsasub:renew-billable-subscriptions
php artisan larsasub:announce-renewal-billable-subscriptions
php artisan larsasub:process-expired-credits
php artisan larsasub:notify-expiring-credits

# Send every mail with sample data (not registered in production)
php artisan larsasub:test-notifications --list

With scheduling.enabled off, the scheduled commands do nothing, also when you run them by hand. See Artisan commands for the full list.

Next steps