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).

Billable models

This page covers how your own models become billable: the contract and traits, the Billable registration the package hangs all billing state on, the three ways a user is mapped to "the one who pays", and the tenancy, invoice-profile and erasure rules that come with it. Read it before you configure the billable block in config/larsasub.php.

The BillableModel contract

A model becomes billable by implementing Contracts\BillableModel and using Traits\IsBillableModel:

use PetersDevelopment\LarsaSub\Contracts\BillableModel;
use PetersDevelopment\LarsaSub\PaymentProviders\Mollie\Traits\IsMollieBillableModel;
use PetersDevelopment\LarsaSub\Traits\IsBillableModel;

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

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

The contract asks for the three accessors above plus getNotifiableUsers(), the people who receive the subscriber's billing mail. IsBillableModel implements that one for you: the model itself when it uses Laravel's Notifiable, otherwise its billable_email as a mail route. Override it to mail team owners or billing contacts instead — see Notifications.

IsMollieBillableModel is required with the Mollie provider: the Billable intermediary delegates getMollieCustomer() and friends to the owner model.

For activeSubscription(), hasFeature() and featureValue(), add Traits\HasSubscriptions as well — see Traits and helpers.

Boot refuses a billable.model (or billable.platform_model) that does not exist or does not implement BillableModel.

The Billable intermediary

LarsaSub does not hang billing state off your model directly. It uses a Models\Billable record in larsasub_billables as an intermediary:

Owner (User, Team, Tenant)
    ↓  owner_id / owner_type (polymorphic)
Billable  (larsasub_billables)
    ↓
BillableSubscription · RecurringCharge · SinglePayment · BillableCredit · MollieCustomer · Invoice

Every billing entity references the Billable's id, never the owner's. That is what lets the same package serve a User-billed app and a Team-billed app without either schema knowing about the other, and what makes per-tenant owner databases possible at all.

Registrations are created lazily by BillableResolver::getOrCreateBillable() on the first billing operation — you never create one by hand.

The registration is also what the package hands around on paths without a request — the notification queue, the scheduler, webhooks — and it answers for its owner there: getNotifiableUsers(), getNotifiableUsersFor($notification), getTenantNotifiableUsers() and getTenants() on a Billable delegate to the owner model, so the owner's own implementation is what runs, once the owner is reachable. See Notifications.

Each registration carries a profile_code: a unique, human-quotable code (A7K3MX2P, or LCRA-A7K3MX2P when a tenant reference code applies) used across the dashboards and in support conversations. It also stores the customer's locale, captured from your owner model — see The customer's locale.

Working with a registration

$billable->owner;                    // your model (may be unreachable outside a tenant context)
$billable->tenant;                   // the stored tenant, or null
$billable->subscriptions;            // BillableSubscription
$billable->recurringCharges;
$billable->singlePayments;
$billable->credit;                   // BillableCredit
$billable->mollieCustomers;

$billable->resolveTenant();          // the stored tenant first, the owner's relation as fallback
$billable->isPlatformBillable();     // dual billing: owned by billable.platform_model
$billable->payableCharges();         // what the customer can pay manually right now
$billable->outstandingCharges();     // unpaid charges to show — a show list, not a pay list

Payable and outstanding charges and mandate setup status are described on their own pages.

Direct and indirect billing

In the default auto resolution mode, BillableResolver::resolve($user) maps the authenticated user to exactly one billable owner, in one of two ways:

'billable' => [
    'model' => \App\Models\User::class,
    'relation' => null,        // direct: the User IS the billable
    // 'relation' => 'team',   // indirect: $user->team() is the billable
],

Direct is the default: the authenticated model holds the subscription.

Indirect points at a relation name on the authenticated model. Several users then share one billable — the team/company scenario. The relation must return a model that itself implements BillableModel and uses IsBillableModel, and billable.model names that class:

'billable' => [
    'model' => \App\Models\Company::class,
    'relation' => 'company',
],

A user whose relation returns null has no billable owner (resolve() returns null). A relation method that does not exist, or that returns a model not implementing BillableModel, throws a BillableResolutionException.

Explicit mode

Apps where one authenticated user can own many billables — a multi-tenant SaaS where each tenant is its own billable and the current one comes from the URL subdomain — should use explicit mode:

'billable' => [
    'model' => \App\Models\Tenant::class,
    'relation' => null,
    'resolution_mode' => 'explicit',   // or LARSASUB_BILLABLE_RESOLUTION_MODE=explicit
],

In explicit mode:

Boot refuses a resolution_mode other than auto or explicit.

The owner resolver

billable.owner_resolver is the optional bridge back to the bundled UI. It names an invokable class that returns the current owner from your own request context:

use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;

class CurrentTenantOwnerResolver
{
    public function __invoke(?Authenticatable $user): ?Model
    {
        return app()->bound('currentTenant') ? app('currentTenant') : null;
    }
}
'owner_resolver' => \App\Support\CurrentTenantOwnerResolver::class,

Return null when there is no current owner (landlord routes, for example); anything else must be a model implementing BillableModel. With the hook configured, ownership policies and the bundled dashboards work in explicit mode. Without it — or when the hook throws or returns something else — ownership checks fail closed: they deny, never 500. Boot refuses an owner_resolver that is not an existing invokable class. A headless host that calls the services itself does not need it: the service calls that check ownership take skipOwnershipCheck: true (below).

This resolver reads request state on purpose. Do not copy the pattern to notifications.payment_url or notifications.first_payment_url: those run in the queue worker, where there is no request and no current tenant.

Passing the owner yourself

The subscribe entry points take the owner model (or the Billable itself), so you hand them the owner your own middleware resolved:

$billable = BillableResolver::getOrCreateBillable($tenant);

$provider->createSubscription($tenant, $plan, $paymentMethod);

$subscription->belongsToBillable($tenant);   // ownership check without a resolvable user

See Subscriptions.

Dual billing

When the platform bills tenants and tenants bill their own users, set both:

'billable' => [
    'model' => \App\Models\User::class,                  // tenant's users → Connect account
    'platform_model' => \App\Models\Organization::class, // platform bills this → global key
],

MollieClientResolver routes on that distinction: payments for platform_model go through the platform's own Mollie key, payments for model through the tenant's Connect OAuth token. Both classes must implement BillableModel and use IsBillableModel. Application fees are skipped for platform self-transactions. See Mollie Connect.

BillableResolver

Helpers\BillableResolver maps users to owners and owners to registrations:

use PetersDevelopment\LarsaSub\Helpers\BillableResolver;

// Owner-model resolution (auto mode, or explicit mode with an owner_resolver)
$owner = BillableResolver::resolve($user);        // User, or Team/Company per config
$owner = BillableResolver::fromRequest($request); // the user of the package's own guard
$direct = BillableResolver::isDirect();

// Billable registrations
$billable = BillableResolver::getBillable($owner);         // read-only; null when absent
$billable = BillableResolver::getOrCreateBillable($owner); // lazy creation, for write contexts

// Connect helpers
$tenant = BillableResolver::deriveTenant($owner);          // via connect.tenant_relation
$code   = BillableResolver::resolveTenantCode($owner);     // e.g. 'LCRA', or null

getBillable() and getOrCreateBillable() both take an optional second argument, the tenant. Pass it whenever you are outside request context — see Tenant-scoped registrations.

Use getBillable() in read paths so a viewer never mints a registration as a side effect of looking at a page, and getOrCreateBillable() in write paths (subscribing, paying, saving a profile). getOrCreateBillable() throws a BillableResolutionException for an owner that does not implement BillableModel, and refreshes the stored locale from the owner on every call.

fromRequest() reads the guard the package is configured for (standalone.guard), not the default one.

The invoice profile

The registration stores the billing profile the invoices are made out to:

Field Notes
invoice_type personal or business — the B2B marker
invoice_first_name, invoice_last_name, invoice_email
invoice_company_name, invoice_vat_number, invoice_coc_number Business profiles only
invoice_address_line_1, invoice_address_line_2, invoice_city, invoice_postal_code, invoice_state
invoice_country ISO 3166-1 alpha-2 — the source of truth for every VAT decision
vat_check_status, vat_check_number, vat_check_reference, vat_checked_at The VIES check of the VAT number (opt-in) — written by the package only, never fillable; see Validating VAT numbers
$billable->invoiceDetails;                  // the assembled array the invoice snapshot reads
$billable->populateDefaultInvoiceDetails(); // seed empty fields from the owner model

invoice_type is load-bearing, not cosmetic. It decides both whether the three business fields are part of the invoice snapshot and whether the EU reverse charge can apply.

Switching away from business clears Billable::BUSINESS_ONLY_FIELDS on every model save. That is enforced on the model rather than in one service, so host code, Nova and Filament are covered too, and a form that hides those inputs but still submits them cannot resubmit a stale VAT number.

Write the profile through the model. Query-builder mass updates dispatch no model events and bypass that rule, exactly as they bypass the immutability guards. It is survivable — the zero rate needs invoice_type === 'business' as well, so a surviving number grants nothing on its own — but prefer model saves.

Whether a supplied VAT number is genuine is what the opt-in VIES check answers (invoicing.vat_validation): the same model hook resets the check on every change of type, country or number — a re-spelling of a number VIES confirmed keeps the confirmation — and, with the check on, queues a new one after the save commits. With the check on, only a number VIES confirmed is reverse charged. See Validating VAT numbers and Compliance.

Tenant-scoped registrations

larsasub_billables carries tenant_type/tenant_id plus a non-null tenant_key ("{morph}:{id}", '' for no tenant) that is part of the unique owner key. Customer #1 of tenant A and customer #1 of tenant B therefore get distinct registrations — which is what makes per-tenant owner databases safe, where owner ids are no longer globally unique.

When you omit the $tenant argument, the tenant is derived from the owner's connect.tenant_relation — in Connect mode only, and never for a billable.platform_model owner — which only works in request context. Outside it (webhooks, queue workers, scheduled commands) pass the tenant explicitly.

Billable::resolveTenant() prefers the tenant stored on the registration. That read never touches the owner model, which is what breaks the chicken-and-egg of needing the owner to find the tenant whose database the owner lives in.

Platform billables and simple-mode registrations have tenant_key = '', unless you passed a tenant to getOrCreateBillable() yourself. Owner-side relations (larsasubBillable(), mollieCustomers(), …) filter on the derived tenant key automatically, so they find a registration only under the tenant that derivation names.

If your owner models live in per-tenant databases, read Multi-database tenancy next — it covers the connect.tenant_context hook that the webhook, queue and scheduler paths need.

The morph map

LarsaSubServiceProvider::boot() registers a Relation::morphMap() alias for every package model (larsasub_subscription, larsasub_billable, …). This keeps class names out of morph columns and makes the package work in host apps that call Relation::enforceMorphMap() — without the host having to enumerate LarsaSub's models.

You only map your own models, for example 'user' => User::class to cover larsasub_billables.owner_type. A package model you already mapped under an alias of your own keeps it: the package registers no second alias for that class, so existing rows and new writes use the same one.

Erasing a billable

anonymize() implements the erasure side of a GDPR request. The registration row itself survives, because the billing history hangs off it.

$billable->anonymize();

It throws a LarsaSubException while the billable has an active or upcoming subscription — cancel or end it first. Otherwise, in one transaction, it:

It leaves the facts a tax authority needs — amounts, rates, VAT treatment, reverse-charge flags — intact, and invoices and credit notes keep their own frozen customer snapshot under the fiscal retention duty.

What it does not do is decide when erasure is lawful, erase data held at the payment provider, or erase anything in your own tables. Read Compliance before wiring it to a button.