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

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:

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 of BillableSubscriptionEndedEvent, the credit expiry events, and the dunning events (DunningResolvedEvent from close() 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:

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: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-reset is 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.