Documentation for larsasub v0.38.2.
On an older version? Your release notes ship with the package (CHANGELOG.md).
Artisan commands
Every larsasub:* command the package registers: what it does, its arguments
and options, and whether the scheduler runs it. Frequencies, overlap protection
and sizing the execution run are on Scheduled commands.
Installation
larsasub:install publishes the config and the dashboard assets, runs the
migrations and seeds the payment methods. Run it on a fresh install and again
after every upgrade.
php artisan larsasub:install
php artisan larsasub:install --seed --active
| Option | Effect |
|---|---|
--seed |
Seed two example subscription plans (fresh install only) |
--active |
Seed active subscriptions and charges for three existing billables (fresh install only) |
--active-payment-methods |
Activate every seeded payment method (fresh install only) |
--vue-components |
Publish the Vue SFC sources, the modules they import and larsasub.css to resources/vendor/larsasub/, for your own build (never overwritten once published) |
--force-config |
Overwrite an existing config/larsasub.php with the package default |
--skip-migrations |
Do not run migrations |
Running it again is safe, and it is the supported way to publish a new
version's assets and apply its migrations: the built assets are always
re-published, a published config is never overwritten without
--force-config, and the payment-method seeder only adds what is missing. The
three demo-data options are skipped on an existing installation, with a
warning. Details and caveats are in Setup.
Diagnostics
Two read-only commands tell you whether an installation can bill. Both keep working when an invalid configuration or a licence-tier mismatch stops the package from booting (see larsasub:license), so they can name the problem.
php artisan larsasub:doctor
php artisan larsasub:doctor --strict # warnings fail too (for CI)
php artisan larsasub:doctor --live # also make one read-only call to the provider
php artisan larsasub:license
larsasub:doctor
The doctor reports configuration that renders fine and only fails later, and checks the database the scheduled commands depend on. Run it after every upgrade.
It exits 0 when healthy (warnings allowed) and 1 on an error. --strict
also fails on warnings. --live adds a real, read-only round trip to the
payment provider.
What it checks:
- Configuration — the same fatals the package refuses to boot on and the
warnings it logs at boot: among them a
standalone.guardthat noauth:entry in the middleware stack authenticates, aconnect.tenant_contexthook that cannot take a tenant ornull, a mail button with nowhere to point, aplan-chargesorrenew-billable-subscriptionsschedule that collects late, and the reconciliation window (see below). - The model-log language — the language model-log lines are written in
(
locales.log, orapp.localeat boot), and whether it renders them, falls back toapp.fallback_locale, or stores raw keys. - Sales addresses company mail cannot reach — an address in
notifications.saleswithout a user account gets company mail by mail only, so a channel list a company notification reads withoutmailin it drops every such mail. The doctor names the addresses and the lists. - Config keys missing from your published config.
mergeConfigFrom()merges the top level only, so a key added after you published reads asnull— forretention.*that silently disables pruning. - Tables and columns. Every shipped table must exist, with the columns of its model, and no encrypted column may be a JSON column (the database rejects the ciphertext).
- Your billable model carries the provider's trait
(
IsMollieBillableModel), without which every checkout fails at the payment step. - Redirect route names in
redirect.*that are not registered. - Active payment methods — none means nobody can subscribe or pay.
- Cost lines never billed on a subscription that already has charges (a heuristic list to eyeball).
- Operations state — the settlement sync (a stored error per account, a
connected account without a website profile or never synced, a platform
account with no payout seen in the window), Connect drains past their
deadline, reminder series the run did not reach or paused for more than
seven days, a reminder schedule that escalates while nothing listens to
DunningEscalatedEvent, and what the VIES checks left behind (or, with the check off, how many business VAT numbers were never checked). - Connect scopes without
refunds.readorrefunds.write, anddunning.chargeback_follow_up_methodsentries that are no payment method here, orpaypal, whose disputes never reach Mollie. - Published views that predate a feature — a mail layout that ignores the
newer theme roles,
notifications.user_app_urlor the tenant admin audience, and a customer chargeback mail from before follow-ups. - Standalone assets that are not published while a bundled dashboard is enabled.
queue.default = syncoutsidelocalandtesting.scheduling.enabled = false— recurring billing is off.- The reconciliation window —
reconcile_unresolved_after_minutesplus theexecute_charges_frequencyinterval, as one number (see Charges whose outcome was never learned).
The column check matters for a database created before 0.37.1: up to then the
migrations were edited in place, so such a database can have nothing pending
while migrate reports success, and the next scheduled run dies on Unknown column. Since 0.37.1 every schema change is a new migration. The expected columns are derived
from each shipped model's $fillable and cast keys rather than from a
maintained list, so they cannot fall behind the code. Extra columns are not
reported.
larsasub:license
larsasub:license prints the tier this installation declares under
license.tier, whether Mollie Connect is enabled, and whether the two fit. It
is a local read: nothing is sent anywhere.
It exits 1 when Connect is enabled on a tier below platform, and tells you
the two ways out (LARSASUB_LICENSE_TIER=platform, or
LARSASUB_CONNECT_ENABLED=false). In that state the package refuses to boot
for every other command except larsasub:doctor, larsasub:install,
larsasub:license and the framework's own recovery commands (config:clear,
cache:clear, vendor:publish, …).
Scheduled commands
Eighteen commands run on Laravel's scheduler. ScheduledCommandsServiceProvider
registers them for you while scheduling.enabled and scheduling.auto_register
are both true (the default); all you add is the schedule:run cron entry.
Times are in scheduling.timezone, or the app timezone when that is empty.
| Command | Purpose | Default frequency |
|---|---|---|
larsasub:renew-billable-subscriptions |
Convert an auto-renewing subscription past its end date to continuous, in place; runs before plan-charges | daily (00:00) |
larsasub:plan-charges |
Create each subscription's charge on its collection day; runs before execute-charges | daily (00:00) |
larsasub:execute-charges |
Submit planned charges to the provider; first repair what earlier runs left unresolved, last finish Connect disconnect drains | daily (00:00) |
larsasub:end-subscriptions |
Take plan templates past their end date out of the offer | daily (00:00) |
larsasub:detect-started-subscriptions |
Dispatch BillableSubscriptionStartedEvent when a future start date arrives |
daily (00:00) |
larsasub:end-billable-subscriptions |
Deactivate subscriptions past their end date that do not renew automatically (fixed terms, and cancelled subscriptions) | daily (00:00) |
larsasub:cleanup-unused-billable-subscriptions |
Deactivate never-activated subscriptions after cleanup_unused_billable_subscriptions_hours (DeactivationReason::FirstPaymentAbandoned) and remove their never-submitted charges |
twiceDaily (01:00, 13:00) |
larsasub:announce-renewal-billable-subscriptions |
Send renewal announcements, two weeks before the notice-free deadline | daily (00:00) |
larsasub:notify-charges |
Announce upcoming charges, notify_charges_days_before ahead — only charges flagged notify, which no generated subscription charge is (why) |
daily (00:00) |
larsasub:process-expired-credits |
Expire the unspent part of credit past its expiry date | daily (00:00) |
larsasub:notify-expiring-credits |
Warn about credit expiring within notify_credit_expiration_days_before |
daily (00:00) |
larsasub:process-dunning |
Run the reminder series: open series for unpaid charges, take the steps that fell due, send what did not go out. A no-op while dunning.reminders.enabled is off (Reminder series) |
daily (00:00) |
larsasub:prune-model-logs |
Delete model-log entries older than retention.model_logs_days |
daily (00:00) |
larsasub:prune-notification-deliveries |
Delete delivery records older than retention.notification_deliveries_days |
daily (00:00) |
larsasub:prune-dunning-cases |
Delete resolved reminder series older than retention.dunning_days (off by default; what it keeps) |
daily (00:00) |
larsasub:sync-settlements |
Mirror Mollie's payouts and attach each payment to the payout it was paid out in (Settlements) | daily (00:00) |
larsasub:check-vat-numbers |
Check business VAT numbers in VIES: never checked, stuck pending, unavailable, and valid ones due a re-check. A no-op while invoicing.vat_validation.enabled is off (Validating VAT numbers) |
twiceDaily (01:00, 13:00) |
larsasub:validate-mandates |
Re-check usable mandates against the provider and mark revoked ones unusable | weekly (Sunday 00:00) |
Commands due in the same minute run one after another in this table's order, so on a shared minute a term renewed at midnight is planned, and a charge planned at midnight is collected, in the same scheduler run. The frequency keys, the overlap protection and the per-command details are on Scheduled commands.
Several events exist only here.
BillableSubscriptionStartedEvent,BillableSubscriptionRenewedEvent,BillableSubscriptionRenewalAnnouncedEvent, the end-date path ofBillableSubscriptionEndedEvent, the credit expiry events, and the dunning events (DunningResolvedEventfromclose()and a payment aside) are dispatched exclusively by these commands. Without a running scheduler they never fire. See Events.
When a scheduled command does nothing
A scheduled command does nothing for one of three reasons, also when you run it by hand:
scheduling.enabled = falseturns the run into a no-op: it printsLarsaSub scheduling is disabled.and exits0. The one exception islarsasub:check-vat-numbers --unchecked, the one-off VIES backfill, which runs with the scheduler and the check itself off: check the numbers you already have before you switch the check on.- No
larsasub.providerconfigured stopsplan-charges,execute-charges,sync-settlementsandvalidate-mandateswithNo payment provider configured.and exit1. - A feature switch that is off makes its command a no-op that exits
0:process-dunning(dunning.reminders.enabled),check-vat-numbers(invoicing.vat_validation.enabled), and the three prune commands while their retention isnullor0.
Exit codes
A scheduled command exits 1 when its work throws, with the command's failure
message in front of the error. Three of them handle every item on its own and
only fail at the end, after the rest had its turn, so one bad row never
costs the whole run:
larsasub:plan-chargesplans every subscription on its own. When planning one throws, that subscription is logged with its id and the run carries on, then exits non-zero with the count. A throw before the charge transaction commits rolls it back: nothing is written, and the next run retries — repair the cause within the billing period, because the catch-up only reaches the period containing today. A throw after the commit (applying credit, settling a zero amount locally) leaves that charge in place and it is not planned again: look at that charge. A subscription whose tenant context cannot be established is reported asNOT planned(and one whose hook threw as skipped, for the next run), but neither changes the exit code.larsasub:process-dunningworks the same way per item (a case, a charge it opens a series for, an event it sends again) and exits non-zero when an item threw or a listener of one of its events threw — what committed stays, and a run at least 15 minutes later retries. It also exits non-zero, before doing anything, for--sincewithout--dry-run, an invalid--sincedate, and — while the series is on, or with--dry-run— an invaliddunning.reminders.stepslist or, on a real run, a missingdunning.reminders.since. A registration whose tenant context cannot be established is skipped and reported, with exit0.larsasub:check-vat-numbersexits1when a registration threw, after the others were checked. No answer from VIES is a status (unavailable), not a failure.
larsasub:execute-charges also collects first and fails afterwards when its
look for reversed chargebacks failed, a local settlement threw (a charge
whose subscription no longer bills, or one with nothing left to collect: that
charge is skipped, the rest is collected) or its Connect drain pass failed.
larsasub:validate-mandates, larsasub:sync-settlements,
larsasub:process-expired-credits, larsasub:notify-expiring-credits and
larsasub:prune-dunning-cases log a failing item, go on with the rest and
still exit 0; sync-settlements exits 1 for an --account it cannot
find, or one other than platform while Connect is off.
Options
Most scheduled commands take no options. These do:
php artisan larsasub:process-dunning --dry-run # show what a run would do; writes and sends nothing
php artisan larsasub:process-dunning --dry-run --since=2026-10-01 # preview with another start day than dunning.reminders.since
php artisan larsasub:validate-mandates --limit=500 # this run only; default validate_mandates_limit, else 5000
php artisan larsasub:check-vat-numbers --unchecked # the VIES backfill; runs while the check and the scheduler are off
php artisan larsasub:check-vat-numbers --limit=200 # registrations this run takes up
php artisan larsasub:sync-settlements --days=365 # a wider window, e.g. on the first run
php artisan larsasub:sync-settlements --account=ABCD # one connected account (its reference_code), or --account=platform
php artisan larsasub:sync-settlements --resync --days=180 # walk paid-out payouts again to attach what an earlier pass missed
--dry-run on process-dunning also works while the series is off, but like
any run it needs scheduling.enabled on. A --limit on check-vat-numbers
that is not a number is refused before anything is checked; a non-positive one
falls back to scheduling.check_vat_numbers_limit. A --limit on
validate-mandates that is not a positive number falls back to
validate_mandates_limit, and a --days on sync-settlements that is not a
positive number is ignored with a warning, so the configured window applies.
One-off commands
These run when you start them: never scheduled, and not gated by
scheduling.enabled.
php artisan larsasub:generate-missing-invoices --limit=100 --dry-run
php artisan larsasub:promote-translations nl --dry-run
php artisan larsasub:connect-reset {tenant_type} {tenant_id}
php artisan larsasub:prune-billables --orphaned --days=30 --dry-run
php artisan larsasub:backfill-valued-from --dry-run
Generating missing invoices
larsasub:generate-missing-invoices creates the invoice for every paid charge
that has none — the data migration for an install that switched invoicing on
later. It does nothing while invoicing.enabled is off.
| Option | Effect |
|---|---|
--limit=N |
Generate at most N invoices this run |
--dry-run |
List what would be generated, create nothing |
It skips superseded charges and mandate-setup charges, exactly as the
paid-webhook and on-demand paths do — there is one choke point, so no path can
re-mint an invoice another one refused. A paid chargeback follow-up gets its
invoice like any paid charge. Single payments are not included. It exits 1
when an invoice failed; the others are still generated.
It invoices a charge in full, even when part or all of it was refunded or charged back. Check those charges after a backfill and issue the credit note yourself — see Refunds.
Promoting plan content to translations
larsasub:promote-translations {locale} converts plain-string plan content to
{locale: value} maps when you switch on multilang mode. It needs
locales.supported configured, and {locale} must be in that list.
It promotes name, description and content of the plan templates and
name and description of the subscriptions (soft-deleted rows included),
skips values that already are a map, and logs on each record which fields it
converted. --dry-run reports the count without writing. See
Multilang.
Resetting a Connect organization
larsasub:connect-reset {tenant_type} {tenant_id} prepares a tenant to connect
a different Mollie organization, which the package otherwise refuses.
tenant_type is the morph alias or class name of your tenant model. It needs
connect.enabled, and asks for confirmation unless you pass --force.
It disconnects the tenant at once (payments still awaiting their outcome are flagged, the tokens are revoked), revokes and archives every local mandate, archives the tenant's Mollie customers and clears the stored organization and website profile, so the next connect may pick another organization.
larsasub:connect-resetis destructive by design. After it, every subscriber of that tenant must re-authorize their mandate. Use it only for a deliberate organization switch. See Mollie Connect.
Pruning orphaned registrations
larsasub:prune-billables --orphaned hard-deletes registrations whose owner no
longer exists and through which no money ever moved — the leftovers of a
rolled-back provisional signup — with everything hanging off them. It never
runs on its own: call it from your own cleanup scheduler.
| Option | Effect |
|---|---|
--orphaned |
Required. Without it the command refuses to run |
--days=30 |
Only registrations whose whole history has been quiet this many days; a non-negative whole number, anything else is refused |
--dry-run |
Report what would be pruned, delete nothing |
Do not run it right after removing an owner: the quiet window counts from the
last write in the registration's history, so a fresh rollback stays
too-recent for --days.
Prune or anonymize. Every guard fails closed, and any paid charge, invoice,
credit note or credit balance blocks a registration forever. For a real,
departed customer the route is Billable::anonymize(): the rows stay (fiscal
retention), the person is stripped. See
Compliance.
Why a registration was kept. The report lists, per reason, how many
registrations were skipped. The reasons are the backed values of
Enums\OrphanBlocker — branch on the enum, never on hand-typed strings. Each
registration gets the first guard that applies; the guards run in this table's
order, from money down to the owner, and the three owner reasons exclude each
other. The last three occur only during a run:
| Value | Meaning |
|---|---|
has-paid-payment |
Money moved through it (a paid charge or single payment) — out of scope forever; the route is anonymize() |
has-invoice |
An invoice or credit note exists; financial documents pin the registration |
has-credit-balance |
Its credit balance is not zero |
has-live-subscription |
An active or upcoming subscription exists |
has-live-payment-attempt |
A claimed charge or payment has no final provider status yet (an unresolved claim, or a SEPA collection still settling) |
has-open-dunning-case |
A reminder series is running (active or paused) — the customer is being asked to pay a charge of it |
mandate-setup-in-flight |
A €0,01 mandate verification may still complete (started or parked) |
has-usable-mandate |
A usable mandate exists |
too-recent |
Something in its history changed within the quiet window (--days) |
tenant-restorable |
The stored tenant row still exists but no longer resolves (soft-deleted or suspended) — a tenant that may come back |
owner-still-exists |
The owner row still exists (soft-deleted included) |
owner-unverifiable |
The owner's absence cannot be proven: a tenant-context hook is configured and the registration has no stored tenant, so the owner may live in a database this process cannot reach |
already-gone |
During the run only: a concurrent run removed it first |
tenant-context-unavailable |
During the run only: your tenant-context hook threw — transient, tried again next run |
error |
During the run only: an unexpected failure on this registration; logged, skipped, and tried again next run |
Backfilling valued_from
larsasub:backfill-valued-from reconstructs valued_from on cost lines that
were marked before that column existed. Run it once, after migrating (see
UPGRADE.md); --dry-run reports what it
would repair without writing.
valued_from is the lower end of the stretch a cost line's billed_through
closes. A line without one reads as starts_on — right for every line no
opening clamp ever touched, and exactly wrong for one that a clamped first
charge already marked, which goes on refunding days nobody was charged for.
The command rebuilds the window from the charges' amount_breakdown
snapshots by walking the chain backwards. It touches open lines only (an ended
line can never refund again), makes no provider calls and is idempotent.
It is safe to run while collections are going out. Its write only lands
while valued_from is still empty, billed_through is unchanged and ends_on
is still null; when a concurrent write moved one of them, that write wins and
the line is reported as left to it rather than as repaired or failed. When it
was billed_through that moved, a later run picks the line up again, and the
log names which column moved. The repair and its audit-log line are one
transaction: a failing log rolls the repair back and reports the line as
error.
It never guesses: a valued_from one day too late turns a visible
over-refund into a silent under-refund. Anything it cannot prove is left
untouched and listed (the first 50) with a reason per line — the backed
values of Enums\ValuedFromBlocker, which are contract. That list is how you
tell the lines apart: a line left null because its window provably starts at
starts_on looks identical to one nothing could be proven about. Reasons and
the by-hand recipe are in UPGRADE.md.
Testing notifications
larsasub:test-notifications sends every mail the package can send, built from
in-memory test data, so you can check your theming and wording. It is not
registered in the production environment.
php artisan larsasub:test-notifications --list
php artisan larsasub:test-notifications SubscriptionActivatedUser --to=you@example.com
php artisan larsasub:test-notifications --all --to=you@example.com
php artisan larsasub:test-notifications SubscriptionActivatedUser --scenario=intro --to=you@example.com
--to is required for sending. A mail with several scenarios sends all of
them when you give no --scenario:
| Mails | Scenarios |
|---|---|
| Activation (user and company) | continuous, intro, no-renewal, renewal-direct, renewal-longer, renewal-same, renewal-from-none, free, deferred, paid-deferred |
| Started (user and company) | deferred, free, paid-deferred |
| Renewed | renewed-with-notice, renewed-direct |
| Dunning reminder (user and company) | failed, standalone, follow-up, final |
Every other mail uses the fixed scenario unless you pass another. See
Notifications.