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

Traits and helpers

The traits you put on your own models, the traits the package's models are built from, and the two public helpers (BillableResolver and CountryHelpers). Start with IsBillableModel: every integration needs it on the model that gets billed.

Every trait under Traits\* is public API, and so is PaymentProviders\Mollie\Traits\IsMollieBillableModel — see Public API. Its namespace-mate HandlesMollieWebhooks is internal webhook plumbing.

IsBillableModel

Add it to the model that gets billed (your User, Team or Company). It gives that model the billing relations and helpers, all routed through its Billable registration.

use PetersDevelopment\LarsaSub\Contracts\BillableModel;
use PetersDevelopment\LarsaSub\Traits\IsBillableModel;

class User extends Authenticatable implements BillableModel
{
    use IsBillableModel;
}

The trait supplies getNotifiableUsers() from the BillableModel contract, but not its three accessors (billable_name, billable_email, billable_identifier) — those are yours to write; see Billable models. It also brings in HasModelLogs and TranslatesModelLogs, so the model gets log(), logs() and logKey().

$user->larsasubBillable;          // the Billable registration, or null
$user->getCreditBalance();        // available credit, as a float
$user->payableCharges();          // charges payable right now — the "pay now" source
$user->outstandingCharges();      // every attempted, unpaid charge — a SHOW list
$user->usableMandates($tenant);   // tenant-scoped in Connect mode
$user->mandateSetupStatus($tenant);

Method reference

Method Returns Notes
larsasubBillable() MorphOne The owner's Billable registration, for the owner's own tenant
credit() HasOneThrough The BillableCredit account
creditTransactions() Builder The credit ledger. A query, not a relation: it sits three levels deep
getCreditBalance() float The available balance, through CreditService. One balance in currency.default; it only pays charges in that currency
hasCredit() bool Whether that balance is above zero
singlePayments() HasManyThrough Every SinglePayment
payableCharges() Collection What the customer may pay right now — bind a pay button to this. See Recurring charges
outstandingCharges() Collection Every attempted, unpaid charge; ask each one notPayableReason() before offering it
dunningCases() Collection The owner's reminder series, newest first, every status
providerMandates(?Model $tenant = null) Collection Every mandate, through the configured provider
usableMandates(?Model $tenant = null) Collection The usable ones
hasUsableMandate() bool Takes no tenant
countUsableMandates() int Takes no tenant
mandateSetupStatus(?Model $tenant = null) Enums\MandateSetupStatus Where the latest €0.01 verification stands; None without a registration
latestMandateSetupCharge() ?RecurringCharge That verification charge, for its timestamps and provider status
getNotifiableUsers() Collection Who gets the customer's mail — see below
getTenantNotifiableUsers() Collection Who gets a tenant's company mail in Connect mode. Empty by default
getTenants() Collection Connect mode: the tenant from connect.tenant_relation. Override it to let the dashboards switch tenants
getBillableLocaleAttribute() ?string The billable_locale accessor — see below
logKey(string $key, array $params = [], array $extra = [], array $meta = []) ModelLog A translated history line — see HasModelLogs

Without $tenant the mandate methods look across every account. In Connect mode a billable holds one Mollie customer per tenant, and a call without a tenant returns the mandates of all of them. Pass the tenant to ask about one account. hasUsableMandate() and countUsableMandates() take no tenant, so they always count across all accounts.

Defaults you can override

getNotifiableUsers() returns the model itself when it uses Laravel's Notifiable. Otherwise it returns its own billable_email (with billable_name on the To header) as a route-based recipient, or nobody when that address is empty or not a valid e-mail address. Override it to mail team owners or billing contacts instead, or implement Contracts\ResolvesNotificationRecipients to decide per notification — see Notifications.

getBillableLocaleAttribute() reads a locale attribute when the model has one. Override it to take the content locale from somewhere else (a profile relation, a tenant setting). BillableResolver::getOrCreateBillable() copies its answer onto the registration.

logKey() on your model

The trait adds a public logKey() to your model, next to the log() and logs() that HasModelLogs brings in. A method of the same name on your own model silently replaces the trait's — PHP gives a class method precedence over a trait method, without a warning — and the package writes the owner's history lines through it. Rename yours, or keep the signature logKey(string $key, array $params = [], array $extra = [], array $meta = []): ModelLog.

An owner that implements Contracts\BillableModel without this trait has no logKey(), and needs none: the package writes its lines on the owner through an internal helper that renders them itself when the model uses HasModelLogs, and writes nothing when it does not.

Relations of your own through the registration

Two helpers, hasOneThroughBillable() and hasManyThroughBillable(), are protected: they exist so your model can define its own relations through the registration, not to be called from outside.

class User extends Authenticatable implements BillableModel
{
    use IsBillableModel;

    public function invoices()
    {
        return $this->hasManyThroughBillable(\PetersDevelopment\LarsaSub\Models\Invoice::class);
    }
}

Both take (string $targetClass, string $targetFk = 'billable_id') and add an owner_type and a tenant-key guard, so a registration belonging to another owner class or another tenant can never leak in.

HasSubscriptions

Opt-in, alongside IsBillableModel: it adds subscription relations and feature checks to the owner. It routes through the registration the way IsBillableModel does, so always use both.

use PetersDevelopment\LarsaSub\Traits\HasSubscriptions;
use PetersDevelopment\LarsaSub\Traits\IsBillableModel;

class User extends Authenticatable implements BillableModel
{
    use HasSubscriptions, IsBillableModel;
}
if ($user->hasFeature('max_users')) {
    $limit = $user->featureValue('max_users') ?? 5;
}
Method Returns Notes
activeSubscription() HasOneThrough The active BillableSubscription with the latest start date
upcomingSubscription() HasOneThrough The upcoming one that starts soonest
pendingSubscription() HasOneThrough The newest one neither activated nor deactivated — awaiting its first payment
failedFirstPaymentSubscription() HasOneThrough The newest one deactivated before activation by a failed first payment
featureList() array The active subscription's features; [] without one
hasFeature(string $feature) bool false without an active subscription
featureValue(string $feature) mixed null without an active subscription or the feature

featureValue() here takes no default argument. Resolve a missing value yourself with ?? $fallback. The version on BillableSubscription ($subscription->featureValue($key, $default)) does take one.

Type-hinting against it

Contracts\HasSubscriptions covers all seven methods. The package never type-hints on it, so declaring it is optional — but the trait satisfies the interface in full, so it costs nothing but the implements:

use PetersDevelopment\LarsaSub\Contracts\HasSubscriptions as HasSubscriptionsContract;

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

Add it when your own code, or a package you use, wants to accept "anything with subscriptions" instead of one concrete model.

IsMollieBillableModel

Required with the Mollie provider, next to IsBillableModel (it reads that trait's tenant key). The Billable registration delegates its Mollie customer and mandate calls to the owner model, and throws a LarsaSubException naming this trait when the owner lacks it. larsasub:doctor checks for it up front.

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

class User extends Authenticatable implements BillableModel
{
    use IsBillableModel, IsMollieBillableModel;
}
Method Returns Notes
mollieCustomers() HasManyThrough Every MollieCustomer of the owner — one per tenant in Connect mode
mollieMandates() HasManyThrough Every MollieMandate of the owner
getMollieCustomer() MollieCustomer Get or create the customer for the owner's tenant; creating calls the Mollie API. Throws LarsaSubException on failure
mollieMandate(PaymentMethod|int $paymentMethod) ?MollieMandate The first mandate for that payment method
createMollieMandate(PaymentMethod|int $paymentMethod, string $consumerAccount) ?MollieMandate Creates a mandate at Mollie and records it
recordExistingMollieMandate(PaymentMethod|int $paymentMethod, string $mandateId, string $consumerAccount = '') MollieMandate Records a mandate Mollie already created, as pending; no mandate create call. It goes through getMollieCustomer(), so without a stored MollieCustomer it creates a new, empty customer at Mollie first — record the existing cst_… customer before you import (see Migration)

In Connect mode getMollieCustomer() scopes by tenant automatically, and customers and mandates are created under the tenant's Mollie account through its OAuth token.

HasBillableOwnership

The ownership check the package's own models use. Add it to your own model when it hangs off a billable and you want the same check.

$model->ownedByUser($user);
$model->belongsToBillable($billable, $tenant);
Method Returns Notes
ownedByUser(mixed $user = null) bool Resolves the user's billable owner and its registration; null uses the package's authenticated user
belongsToBillable(Billable|Model|int $billable, ?Model $tenant = null) bool Takes the Billable, the owner model, or a raw billable id

belongsToBillable() exists for callers that hold the billable directly: explicit resolution mode without an owner_resolver cannot resolve a user for ownedByUser(). Both checks fail closed: an unregistered owner, or a user that cannot be resolved, is false, never an exception.

Pass $tenant outside request context. In Connect mode with tenant-scoped registrations, the owner-model form derives the tenant from connect.tenant_relation, which only works in request context. In webhooks, queue workers and landlord routes, pass $tenant explicitly, or pass the Billable or the raw id instead.

Ownership is recognised through a direct billable_id, or through a billable(), billableSubscription or billableCredit relation.

These are instance methods, not query scopes. To filter a query, use forBillable() from Traits\ScopedToBillable.

ConfigurableNotificationChannels

Makes a notification read its channels from larsasub.notification_channels: the per-class entry under overrides first, then default, then mail. A channel the notification has no to…() method for is skipped with a warning, and the database and broadcast channels are dropped for a route-based recipient. Every LarsaSub notification uses it; add it to your own to inherit the same configuration.

use PetersDevelopment\LarsaSub\Notifications\ConfigurableNotificationChannels;

class MyNotification extends Notification
{
    use ConfigurableNotificationChannels;
}

See Notifications → Channels for the config.

HasModelLogs

From peters-development/laravel-model-logger. Every LarsaSub model with a history of its own uses it — the registration, plans, subscriptions, cost lines, charges and payments, invoices, mandates, reminder series and so on; line, ledger and bookkeeping rows (InvoiceLine, CreditTransaction, DunningStep, MollieCustomer, …) do not. It writes a per-model history to larsasub_model_logs.

$subscription->logs;
$subscription->logKey('billable-subscription.log.activated');

Translated lines: TranslatesModelLogs

The package writes its own lines through TranslatesModelLogs::logKey(): a translation key plus scalar parameters, rendered in the fixed log language (locales.log, see Content locales). Unset, that is app.locale as it stands when the package's provider boots — before your AppServiceProvider, so an App::setLocale() there does not change it. Set LARSASUB_LOG_LOCALE (or larsasub.locales.log) when the history should be in another language.

The key and parameters are stored in the row's meta, so you can show a line in another language without rewriting the table:

$log = $subscription->logs()->latest('id')->first();
$params = $log->meta['message_params'];

// The one language-bound parameter: a cancellation's spelled-out end date.
if (isset($params['end_date'])) {
    $params['end'] = \Carbon\Carbon::parse($params['end_date'])->locale('en')->translatedFormat('j F Y');
}

$line = __('larsasub::'.$log->meta['message_key'], $params, 'en');
foreach ($log->meta['message_extra'] ?? [] as $segment) {
    [$key, $params] = $segment;
    $line .= ' '.(isset($segment[2])
        ? trans_choice('larsasub::'.$key, $segment[2], $params, 'en')
        : __('larsasub::'.$key, $params, 'en'));
}

What the stored meta holds:

Lines written before the package translated its logs have no message_key and stay as they were. Provider and exception texts inside a line (:error) are never translated.

Lines on the billable: LogsOnBillable

Models that hang off a registration (BillableSubscription, RecurringCharge, SinglePayment) also use LogsOnBillable: logKeyOnBillable(string $key, array $params = [], array $extra = []) writes the same kind of line on their billable, and does nothing when there is none. logOnBillable(string $message) writes a literal line.

Retention

Entries land in larsasub_model_logs on the package's own connection, and are pruned by larsasub:prune-model-logs after retention.model_logs_days (default 730).

Model traits

The building blocks of the package's own models. They are public, and you can put them on your own models, but most integrations only meet them as methods on the LarsaSub models.

Trait Used by Provides
HasActiveState Subscription, PaymentMethod, TenantPaymentMethod isEnabled() / isDisabled() on the active flag, and the scopes isEnabled() / isDisabled()
HasPaidState RecurringCharge, SinglePayment isPaid() / isNotPaid() on paid_on, and the scopes isPaid() / isNotPaid()
HasSubscriptionLifecycle BillableSubscription The activation state: isActive(), isUpcoming(), isInactive(), isChargeable(), hasFailedFirstPayment(), isCancelled(), willRenew(), willEnd(); the scopes isActive($checkDate), isUpcoming($checkDate), isInactive($checkDate), isActivated(), isNotActivated(), isDeactivated(), isNotDeactivated(), isChargeable(), renewalNotAnnounced(); the activated, deactivated, is_active, is_inactive and is_upcoming attributes
HasSubscriptionFeatures BillableSubscription features(), hasFeature(string $key), featureValue(string $key, $default = null) — config defaults, then the plan's features, then the subscription's feature_overrides; on BillableSubscription the plan is read also when it is soft-deleted
HasIntroductionPeriod BillableSubscription isInIntroductionPeriod(), currentEffectivePrice(), remainingIntroductionPayments(), and the matching is_in_introduction_period, current_effective_price and remaining_introduction_payments attributes
HasTranslatableFields Subscription, BillableSubscription translatableFields(), getTranslation(), getTranslations(), hasTranslation(), setTranslation(), setTranslations(), forgetTranslation() — the spatie-style API over Casts\TranslatableText columns; see Multilingual plan content
ScopedToBillable Models with a billable_id The scope forBillable(Model|int $billable)
ScopedToTenant Tenant-aware models The scope forTenant(?Model $tenant, bool $includeGlobal = false)
SubscriptionScopes Subscription, BillableSubscription The scopes hasEndDate(), hasNoEndDate(), hasAutoRenewal(), hasNoAutoRenewal(), endDatePassed(), endDateNotPassed()
IsImmutableRecord Invoice, InvoiceLine, CreditNote Refuses updates and deletes on an issued document (a LarsaSubException); corrections go through credit notes
LogsOnBillable BillableSubscription, RecurringCharge, SinglePayment logOnBillable(), logKeyOnBillable() — see HasModelLogs
TranslatesModelLogs Every package model with a history, and IsBillableModel logKey() — see HasModelLogs

isActive(), isUpcoming(), isInactive() and isChargeable() exist both as instance methods and as scopes, while isCancelled(), willRenew() and willEnd() are instance methods only. Write every scope chain as Model::query()->isActive(), so it can only mean the scope.

Immutability is enforced on model events. A query-builder mass update or delete dispatches no model events, so it bypasses IsImmutableRecord. Write through the models.

Plumbing traits

The package's policies, dashboard controllers and notification listeners share these. They sit in the public namespace, but you configure what they do rather than use them directly.

Trait Used by What to use instead
LarsaSubPolicyTrait The package's policies Configure authorization through larsasub.policies
BuildsConnectContext The bundled dashboard controllers The tenant resolution in Mollie Connect
ChecksNotificationConfig The notification listeners The notifications.user.* / notifications.company.* toggles and notifications.disabled_listeners

BillableResolver

Helpers\BillableResolver turns an authenticated user into the billable owner model, and an owner into its Billable registration. It is the public helper the package itself uses everywhere.

use PetersDevelopment\LarsaSub\Helpers\BillableResolver;

$owner = BillableResolver::resolve($user);        // the owner model, per config
$owner = BillableResolver::fromRequest($request);
$direct = BillableResolver::isDirect();

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

$tenant = BillableResolver::deriveTenant($owner);
$code   = BillableResolver::resolveTenantCode($owner);      // e.g. 'LCRA', or null
Method Returns Notes
resolve(?Authenticatable $user) ?Model The owner for a user: the user itself, or its billable.relation. In explicit billable.resolution_mode it asks billable.owner_resolver, and throws BillableResolutionException when none is configured
fromRequest($request) ?Model resolve() on the request's user, read through standalone.guard
isDirect() bool Whether the user is the billable itself (no billable.relation)
getBillable(Model $owner, ?Model $tenant = null) ?Billable Read-only lookup; never creates a row
getOrCreateBillable(Model $owner, ?Model $tenant = null) Billable Lazy creation for write contexts; the owner must implement BillableModel. Stores the tenant and the owner's content locale
ownerLocale(Model $owner) ?string The owner's billable_locale, or null
deriveTenant(Model $owner) ?Model The tenant from connect.tenant_relation; null in simple mode and for the platform billable
resolveTenantCode(Model $owner, ?Model $tenant = null) ?string The tenant's four-character reference code, from its connected account

Without $tenant, the registration calls derive the tenant from the owner, which only works in request context; pass it explicitly in webhooks, queue workers and commands. Full treatment in Billable models.

CountryHelpers

Helpers\CountryHelpers is the package's country list and the one place codes are normalised. The code is the source of truth for every VAT decision; names are presentation.

use PetersDevelopment\LarsaSub\Helpers\CountryHelpers;

CountryHelpers::normalize(' nl ');   // 'NL'
CountryHelpers::isEu('NL');          // true
CountryHelpers::display('NL');       // per invoicing.country_display
CountryHelpers::euOptions();         // ['AT' => 'Austria', …] for a select
Method Returns Notes
normalize(?string $countryCode) ?string Trimmed and uppercased; empty becomes null
isEu(?string $countryCode) bool EU member state
isKnown(?string $countryCode) bool A real ISO 3166-1 alpha-2 code from the bundled list, any casing
name(?string $countryCode, ?string $locale = null) ?string The name, or null for an unknown code. A larsasub::countries.{CODE} translation wins
nameOrCode(?string $countryCode, ?string $locale = null) ?string The name, falling back to the code
display(?string $countryCode, ?string $mode = null) ?string As an invoice prints it: invoicing.country_display (name by default, or code); unknown codes print as the code
options(?string $locale = null) array Every country, code => name, sorted by name
euOptions(?string $locale = null) array The EU member states, code => name, sorted by name
all() array The bundled English names, untranslated

See Invoicing and VAT.

NotificationRecipientResolver

Helpers\NotificationRecipientResolver is internal: it is how the package sends its customer mail, described here so you know which of your methods it calls. Do not call it from your own code.

For each customer notification it queues a delivery job rather than calling Notification::send() — that is what makes the tenant context and the delivery log work (see Multi-database tenancy). The job then resolves the recipients on the owner model, in this order:

  1. getNotifiableUsersFor($notification), when the owner implements Contracts\ResolvesNotificationRecipients — the per-notification hook (see Notifications);
  2. otherwise getNotifiableUsers();
  3. otherwise the owner itself, when it uses Laravel's Notifiable;
  4. otherwise nobody, with a warning in the log.

To change who receives the customer's mail, override getNotifiableUsers() or implement the contract.

NotifyRole

Helpers\NotifyRole is internal as well: it sends the platform's company mail to the recipients configured under larsasub.notifications.sales — a role name (requires spatie/laravel-permission) or a list of e-mail addresses. Configure that key rather than calling the class; see Notifications.

This funnel uses the ordinary queued send. That is safe for the shipped shape — plain addresses and a central-database role lookup. If your notifiable users are tenant-scoped, making this path context-aware is yours to do; see Multi-database tenancy.