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.
- IsBillableModel
- HasSubscriptions
- IsMollieBillableModel
- HasBillableOwnership
- ConfigurableNotificationChannels
- HasModelLogs
- Model traits
- Plumbing traits
- BillableResolver
- CountryHelpers
- NotificationRecipientResolver
- NotifyRole
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
$tenantthe 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()andcountUsableMandates()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 onBillableSubscription($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
$tenantoutside request context. In Connect mode with tenant-scoped registrations, the owner-model form derives the tenant fromconnect.tenant_relation, which only works in request context. In webhooks, queue workers and landlord routes, pass$tenantexplicitly, or pass theBillableor 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:
message_keyandmessage_params: the key (without thelarsasub::namespace) and the values the line was written with.message_extra: present only when the line has further sentences (a cancellation reason, a settlement that failed first). Each entry is[key, params], or[key, params, count]for atrans_choiceline.- The one parameter that is already language-bound is
endon the cancellation lines (billable-subscription.cancelled*), a date spelled out in the log language. Those lines also carryend_date(ISO), which the example formats in the target language.
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:
getNotifiableUsersFor($notification), when the owner implementsContracts\ResolvesNotificationRecipients— the per-notification hook (see Notifications);- otherwise
getNotifiableUsers(); - otherwise the owner itself, when it uses Laravel's
Notifiable; - 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.