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

Compliance: what LarsaSub does, and what stays yours

LarsaSub automates subscription billing; it does not make you compliant. This page lists the obligations a consuming application inherits, and for each one what the package handles and what you must arrange yourself. Read it before you go live, and again when you change how amounts, mandates, notices or documents work. Items marked yours have no package support today.

SEPA Direct Debit

Every recurring collection runs on a mandate, and SEPA requires the debtor to be told the amount and date before a collection. The package sends the notice for a fixed series and for changed amounts; the creditor role, your mandate terms and any per-collection promise stay yours.

Pre-notification

The package provides: a one-time pre-notification for a fixed recurring series. The activation email states the amount to be debited — the plan price plus any cost lines active on the first billing day, VAT-converted exactly as the charge will be, and labelled as such — the introduction price and how long it lasts, the price after it, the start date and the next payment date. That satisfies the rulebook's notice requirement for a series whose amount does not change. The amount of a series does change when a cost line or the VAT rate moves it; those changes are announced as below.

The debited amount is what the notice must state, and it is not always the listed price: on a VAT-exclusive install the collection is the listed price plus VAT, and during an introduction period the charge bills the introduction price. The activation, started, renewal-announcement, renewed, plan-change and VAT-rate-change mails all resolve their amount through Support\MonthlyAmountSummary, so they cannot state different figures for the same subscription. The renewal announcement is a pre-notification in its own right: it announces a future collection on its own scheduled path.

When the notice reaches the customer depends on how the subscription started:

Flow First collection from the mandate
Checkout (createSubscription()) The customer pays the first period at the checkout; the activation mail follows, and the first collection from the mandate is the next period's — a billing period or more after the notice
Deferred start (createDeferredMandateSubscription()) Activated, and mailed, right away; the planner collects from start_date
Mandate reuse (createSubscriptionWithMandate()) The first collection is submitted at once, and the activation mail goes out when it is paid — after the debit, not before it

Yours:

Changed amounts

The package provides: when the amount changes through a cost line (add, rate change, end), an email with the new amount per period (notifications.user.subscription_cost_changed, on by default) — once per change act, listing everything that moved. The mail goes out when the change is made, and the next charge collects the new amount on its collection day, so a change made shortly before a collection day is announced shortly before the debit.

One category is deliberately not mailed as a change: the subscription's first set of cost lines, on a subscription that has never been charged and whose lines take effect on or before its start date. That is the amount the activation notice itself states, so it is covered by that notice rather than by a correction to it. Everything after that is announced. See Cost lines for the exact rule and the one residual gap (a subscription that starts with no cost lines and gains its first before its first charge).

When the VAT rate of a plan changes through the model, the subscribers whose debit it moves (exclusive prices for a customer who pays VAT, or a zero-rated customer on inclusive prices) get an email with the new amount per period (notifications.user.subscription_vat_rate_changed, on by default), running and upcoming subscriptions alike. There is no lead time: the next charge is composed at the new rate. See VAT rate changes.

Yours: a change of the plan price (as opposed to a cost line) is a host decision: announce it, and offer termination where consumer law requires. So is the lead time before a VAT rate change, and the announcement of a change of invoicing.vat_rate, which the package cannot observe.

Chargebacks

The package provides: with dunning.chargeback_follow_up on (the default), a SEPA chargeback after settlement becomes a follow-up charge the customer pays themselves, through a checkout. The package never re-collects it from the mandate: that would need a fresh pre-notification, and after an MD01 ("no valid mandate") it may not be collected from that mandate at all. The credit note for the charged-back invoice is issued as before; the follow-up is invoiced when it is paid.

When the bank reverses the chargeback, an unpaid follow-up is withdrawn. A paid one means the customer paid the period twice, and dunning.double_paid decides what happens: signal (the default) flags it so the operator is told and gives it back; refund refunds it, normally on the original payment, so the follow-up's invoice stays the customer's document; credit returns it as wallet credit. A reversal that meets no paid follow-up leaves the chargeback's credit note standing while the money is back: the package has no debit note, so your bookkeeping corrects it.

Yours: the bank's chargeback fee (Mollie charges one per SEPA chargeback) and any collection costs or interest are not passed on by the package; whether and how you charge them is a decision under your own terms.

Reminders

The package provides: the reminder series (off by default), which reminds and nothing more. Its mails state the amount open and how to pay it; they name no consequences, costs or deadlines, and an escalate step is an event to your application — not a notice of default. The package never suspends access, never re-collects a failed charge on its own, and never adds costs.

Yours:

VAT & invoicing

With invoicing on, the package decides the VAT of every charge and issues the invoices and credit notes. Invoicing ships off; turning it on with an empty seller VAT number logs a warning and larsasub:doctor flags it.

The package provides:

With invoicing off the package converts no amount: what is listed is what is debited.

Collected equals invoiced

The VAT the invoice states is always contained in the value that was settled; the package never invoices more than it collected, in both price modes. Where credit was applied, the invoice states the full service value and total - credit_applied is what the customer was debited — the credit is part of the settlement, not a discount on the taxable amount.

VAT numbers checked in VIES

The package provides: an opt-in check of business VAT numbers in VIES (invoicing.vat_validation, see Validating VAT numbers). On, the reverse charge applies only to a number VIES confirmed, for that very number; until then the business customer pays VAT and reclaims it, so no VAT is lost on a number nobody verified. The check is repeated (by default every 30 days). The consultation number VIES issues to you as requester is frozen with the sale and kept on the reverse-charged invoice — also after erasure — and a charge keeps the decision of the day it was created.

In every mode the profile and the document agree: the reverse charge requires invoice_type = 'business' — a stored number alone grants nothing — and a profile that is not business has its company name, VAT number and CoC number cleared on save, whatever writes it. A query-builder mass update dispatches no model events and skips that clearing; the type requirement still holds, so no zero rate follows from it, and a VIES status only counts for the number it was decided for.

Yours: with the check off, the reverse charge is applied on the number the customer typed. Validate it and keep the confirmation, or the VAT is yours to pay — or switch the check on (backfill first, see Validating VAT numbers). Either way, comparing the customer's name and address, which art. 18(1)(a) of Regulation 282/2011 also asks for, stays yours: the package does not read or store what VIES returns for them. A VIES confirmation does not by itself establish the right to the exemption.

VAT rate changes

The package provides: a subscription charge takes the VAT rate in force when it is composed, on its collection day: the subscription's own override, else the plan's rate, else invoicing.vat_rate. A subscription never keeps the rate it was signed up at, so a legal change, or a corrected rate on a plan, reaches every running subscriber from their next charge (Directive 2006/112/EC, art. 65 and 93: for a payment on account, the rate on receipt). Paid charges and their invoices keep the rate they were collected at. When a plan's rate changes through the model, the subscribers whose debit changes are mailed the new amount (notifications.user.subscription_vat_rate_changed); see Changed amounts.

Standalone charges and single payments fix their VAT when they are created.

Yours:

Your VAT obligations

Selling to consumers

Consumer law adds rules on notice periods, on what the order button says, and on the right of withdrawal. The bundled dashboards carry the package's part of each; a headless host owns its own screens and with them these duties.

Notice periods

The package provides: a default cap on notice periods. Plans a tenant defines through the bundled dashboard are refused with a notice period longer than subscription.max_notice_period_months (default 1 — NL BW 6:236 sub j voids a longer period after silent renewal). A cancellation inside the notice window genuinely extends the contract and bills the extra months, which is why the cap is a money setting, not cosmetics.

The cap is one package-wide number, and only a validation. It applies to both notice-period fields and cannot vary per tenant, per plan or per customer type. Raising it for B2B therefore also removes the consumer-safe default for every B2C subscriber on the installation, and disabling it (null) removes it everywhere. It is checked in the bundled tenant plan form only: plans you create in code or an admin panel are not capped, and existing plans with a longer period keep it.

Yours: a platform that sells to both audiences needs its own per-tenant or per-plan policy on top — enforce it in your own admin flow before you raise this value.

The order button

The package provides: an order button on the bundled subscription dashboard that names what the click collects (NL BW 6:230v, EU Consumer Rights Directive art. 8). Each plan card prints the quote of its first charge — VAT added or stripped as the charge will, the lines a before_first_charge hook adds, an introduction price and what follows it — with the VAT label the subscription mails use, and repeats the amount directly above the button. On a paid plan the button reads "Order with obligation to pay" / "Bestelling met betalingsverplichting", the statutory formula; a free plan keeps "Get started".

Where no amount can be named there is no button: the card says why (enter a country first on exclusive prices, add a payment method first, or a hook that cannot state its lines), and the server refuses the same subscribe — before any terms evidence or registration exists. The texts are built into the bundle; a host that rebuilds the components with its own wording owns that wording.

Yours: the amount on the button, in your own checkout. A headless flow, or a host that replaced the bundled subscription dashboard, owns its confirm step and has to name the amount that will be charged there. Four things can put the debit somewhere other than the catalogue price: an introduction price, cost lines added by a before_first_charge hook, exclusive prices (invoicing.prices_include_vat set to false: the charge grosses up for every customer the country rules do not zero-rate), and — with invoicing on and the default inclusive prices — a zero-rated customer (reverse charge, outside the EU), whose charge has the VAT taken off.

The package gives you the amount, not the screen: SubscriptionCostService::quoteFirstCharge() returns it from the charge's own calculation, which is the only place it can come from — VAT is converted per breakdown row and re-summed, so a host that grosses up its own total can be a cent or two off. Print its rows and its total, and see Cost lines for the three flags that say the total is not the whole story — one of which, creationRefused, says the subscription cannot be created at all, so there is no order button to render rather than one with a different number on it. On installs with invoicing.prices_include_vat = false the quote refuses to answer without a customer country rather than name an amount short by the full VAT rate.

Terms and the right of withdrawal

The package provides: with a terms provider configured (terms.provider, none by default), the bundled subscribe flow records each acceptance — the version, the language it was shown in, the time, and a keyed one-way hash of IP address and user agent, never the raw values — before the payment starts. With terms.withdrawal_consent on, it also requires and records a second, separate consent: to immediate performance, and acknowledgement of losing the 14-day withdrawal right. See Terms acceptance.

Yours:

Privacy (GDPR)

The package gives you an erasure path, retention settings for what it logs, and a record of what each table holds. The controller's duties — securing the database, your privacy notice, erasure at Mollie — stay yours.

Erasure

The package provides: Billable::anonymize(), the erasure path. It refuses (with a LarsaSubException) while the registration has an active or upcoming subscription — cancel or end it first. Otherwise, in one transaction, it:

Invoices and credit notes keep their frozen customer snapshot, under the Art. 17(3)(b) legal-obligation exception. The detail bags are stripped rather than emptied: the rest of them (payment id, status, amounts, timestamps, settlement) is how a payment stays reconcilable long after the person is gone, and erasure has no reason to cost that.

Two things erasure does not touch:

Orphaned registrations are deleted outright, terms evidence included. larsasub:prune-billables --orphaned (host-invoked, never auto-scheduled) hard-deletes a registration whose owner no longer exists and through which no money ever moved — the leftovers of a rolled-back provisional signup. Data minimization favors deletion there: the rows tie to nobody reachable, and no obligation remains that could ever need them. That includes the larsasub_terms_acceptances rows: the acceptance evidences consent by an owner that no longer exists for a subscription that never collected anything, and the restrict FK leaves no way to keep it while removing the registration. A registration with ANY financial history (a paid charge or payment, an invoice, a credit note, a credit balance) is never pruned — its route is anonymize(), which keeps the rows and strips the person. See Artisan commands.

Retention

What the package logs is pruned on a schedule you control (the retention block, see Configuration):

Data Kept Pruned by
Model logs (the per-record activity trail) retention.model_logs_days, default 730 days larsasub:prune-model-logs
Notification-delivery records retention.notification_deliveries_days, default 365 days larsasub:prune-notification-deliveries
Resolved reminder series retention.dunning_days, default null (kept) larsasub:prune-dunning-cases

null or 0 disables a prune. The prunes run on the scheduler, so with scheduling.enabled off nothing is pruned. Invoices, credit notes and paid payments are never pruned.

What the operational tables hold

The reminder series (larsasub_dunning_cases, larsasub_dunning_steps). Bookkeeping about how an unpaid charge was pursued: the charge and the billable by id, the origin, the schedule, the status and why it paused or closed, and per step the action, its result and when it ran. No name, address, amount or message text — nothing a person can be read from without the billable it points at, the same class as the charges themselves. anonymize() therefore leaves them alone (it strips the person from the billable they point at), and larsasub:prune-billables --orphaned deletes them with the registration. Retention: retention.dunning_days, default null (kept), because the series documents a debt collection; the prune never deletes a series whose charge is still an unpaid debt.

A Connect OAuth flow while it runs (larsasub_connect_flow_states). This one is transient rather than administrative: a row exists only between the moment a tenant owner starts the Mollie authorization and the moment it completes, is declined, or expires.

Stored Not stored
The tenant the flow will connect (tenant_type + tenant_id) Anything about the tenant beyond that reference
The initiating user's auth identifier (user_key) — who, and only who, may finish this flow Their name, address or any other attribute
SHA-256 of the OAuth state and of the completion ticket Either secret in a form that could be replayed from a dump
The Mollie authorization code, encrypted at rest, between callback and completion The code in the clear — and it is deleted the moment it is exchanged
The origin and return URL the flow started from

There is no retention setting and no prune command, and expiry and deletion are two separate events:

anonymize() deliberately ignores this table — by the time erasure is a question, a flow that concerned that billable is long gone, and a live one belongs to a tenant administrator rather than to the subscriber being erased.

The notification-delivery log (larsasub_notification_deliveries), and what it deliberately does not hold:

Stored Not stored
Notification class (e.g. PaymentFailedNotification) Subject line
Channel, status, attempt count, timestamps Message body, or any hash of it
Recipient as a reference (recipient_type + recipient_id) The recipient's email address
A keyed HMAC-SHA256 hash for recipients that have no model The address behind that hash
The exception class of a failed delivery Its message — transport errors quote the address they could not reach
The billable and tenant the notification concerned
The random delivery_key of the queued job that produced the row (it identifies a job, not a person, and carries nothing derived from one)
Laravel's notification_id, derived from that key and a keyed hash of the recipient (any form of the recipient that could be checked against a guess without the APP_KEY)

Two choices worth stating, because they look like omissions:

A delivery that is still in flight when anonymize() runs is recorded without its billable link: the claim takes the same row lock erasure holds, so the two serialise instead of racing.

Both hashes are keyed with APP_KEY — the recipient_hash column and the fingerprint inside notification_id. The second matters more than it looks: the id is stored next to the delivery_key it is built from, so an unkeyed fingerprint would make the pair a verification oracle. Someone holding a database dump could take an address they were curious about, recompute the id, and a match would tell them that address was mailed — without ever needing the key. Keying it means a guess cannot be checked. Rotating APP_KEY makes older hashes unrecognisable; the only consequence is that a delivery in a retry window could be sent twice.

Your obligations as controller

Payouts and third-party funds

The package never pays out and never holds money. Every payment goes from the customer straight to a Mollie account — yours in simple mode, the tenant's under Connect, where Mollie itself credits your application fee to your own balance. There is no payout run, no balance per seller, no payment split across sellers, and no money moved between Mollie balances; larsasub:sync-settlements only mirrors the payouts Mollie makes itself.

Yours: if your platform collects money for others and pays it on, you hold funds that are not yours, which in the EU is generally a regulated payment service (a licence, or an exemption you establish yourself) — and a different product from this one. Under Connect that question does not arise in this form: each tenant is Mollie's own customer and passes Mollie's own verification (Onboarding status). See What LarsaSub fits, and what it does not.

Connect marketplaces

The package provides: per-tenant seller snapshots and per-tenant invoice sequences, so each tenant invoices as their own merchant of record.

Yours: