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

Invoicing and VAT

LarsaSub is payment-first, not an invoicing tool — but every paid charge can produce a numbered invoice, and every refund or chargeback a credit note. This page covers switching that on, how VAT decides what is collected and what is printed, numbering, VAT-number validation, and the built-in PDFs.

Invoicing is off by default, deliberately. Invoices are numbered, legally relevant documents; switching them on is a conscious decision that comes with configuring the VAT rate and the seller details. A fresh install must never silently issue Dutch-VAT invoices with an empty seller. With invoicing off, the package also converts no amount for VAT: every subscription charge collects the listed price.

Enabling invoicing

Set LARSASUB_INVOICING_ENABLED=true and fill in the seller. The block in config/larsasub.php:

'invoicing' => [
    'enabled' => env('LARSASUB_INVOICING_ENABLED', false),
    'vat_rate' => env('LARSASUB_INVOICE_VAT_RATE', 21.0),
    'prices_include_vat' => env('LARSASUB_PRICES_INCLUDE_VAT', true),
    'price_display' => env('LARSASUB_INVOICE_PRICE_DISPLAY', 'exclusive'),
    'country_display' => env('LARSASUB_INVOICE_COUNTRY_DISPLAY', 'name'),
    'seller' => [
        'name' => env('LARSASUB_INVOICE_SELLER_NAME'),       // falls back to config('app.name')
        'vat_number' => env('LARSASUB_INVOICE_SELLER_VAT'),
        'coc_number' => env('LARSASUB_INVOICE_SELLER_COC'),
        'address' => env('LARSASUB_INVOICE_SELLER_ADDRESS'),
        'country' => env('LARSASUB_INVOICE_SELLER_COUNTRY', 'NL'),
    ],
    'number_prefix' => env('LARSASUB_INVOICE_PREFIX', 'INV'),
    'credit_note_prefix' => env('LARSASUB_CREDIT_NOTE_PREFIX', 'CN'),
    'vat_validation' => [ /* see Validating VAT numbers */ ],
],

Boot refuses a price_display other than exclusive/inclusive and a country_display other than name/code, and warns when invoicing is on without a seller VAT number.

In Connect mode the seller can differ per tenant: implement getInvoiceSellerDetails() on your tenant model, returning name, vat_number, coc_number, address and country, and it takes precedence over the config block. Its country is also the seller country the VAT rules run against.

The built-in PDFs need barryvdh/laravel-dompdf. It is a suggested dependency, not a required one — see Using your own invoicing system.

When an invoice is created

Trigger Path
A RecurringCharge is paid Automatic, from the paid webhook (or a local settlement, such as a charge covered by credit)
A SinglePayment invoice is requested On demand only: the payments dashboard's invoice button, or InvoiceService::createForSinglePayment()
Backfilling existing data php artisan larsasub:generate-missing-invoices
use PetersDevelopment\LarsaSub\Service\InvoiceService;

$service = app(InvoiceService::class);

$invoice = $service->createForCharge($charge);          // ?Invoice
$invoice = $service->createForSinglePayment($payment);  // Invoice

Both are idempotent — a payment carries at most one invoice (invoiceable_type + invoiceable_id is unique), and a second call returns the existing one. Neither checks invoicing.enabled or whether the payment is paid: the automatic path, the command and the download routes do. Call them yourself only for a paid payment.

When the automatic invoice fails, the payment still stands: the error is logged and the charge is flagged needs_attention, which reaches your operators through the company mail. Run larsasub:generate-missing-invoices once the cause is fixed.

larsasub:generate-missing-invoices invoices every paid RecurringCharge without an invoice (mandate-setup charges and superseded charges excluded; single payments are not covered). It takes --limit= and --dry-run, does nothing while invoicing is off, and invoices a charge in full even when part or all of it was refunded or charged back — check those afterwards and see Refunds → Credit notes.

One category is never invoiced: mandate-setup charges. The €0.01 verification payment is not a delivery, so createForCharge() returns null for it. That single choke point is deliberate — all three generation paths (the paid webhook, larsasub:generate-missing-invoices, the on-demand download) run through it, so no path can mint an invoice for it and consume a number.

createForCharge() also returns null for an unpaid chargeback follow-up whose chargeback was reversed: nobody owes it.

The Invoice model

Models\Invoice (larsasub_invoices) is a finalized snapshot. It records the seller, the customer and the amounts as they were at the moment it was created. The model enforces that: an update throws a LarsaSubException, except the move from finalized to voided and the external download URL, and an invoice cannot be deleted — correct one with a credit note.

Property Description
invoice_number Unique, sequential — see Numbering
status finalized or voided
billable_id The Billable registration
invoiceable_type / invoiceable_id The charge or single payment (unique)
tenant_type / tenant_id The tenant, in Connect mode
customer_name, customer_email, customer_company_name, customer_vat_number, customer_coc_number, customer_address, customer_country Customer snapshot
customer_vat_check_reference The VIES consultation number the reverse charge rests on — only on a reverse-charged invoice with the check on, and only when VIES issued one; kept after anonymize(), not printed on the PDF. See Validating VAT numbers
seller_name, seller_vat_number, seller_coc_number, seller_address, seller_country Seller snapshot
subtotal, vat_amount, total, currency Amounts — total is the full invoiced value, in the currency the payment was collected in
vat_rate Invoice-level rate; null when the lines carry a mixed split
reverse_charge Whether the EU reverse charge was applied
credit_applied Credit consumed; the customer was debited total - credit_applied
description, payment_method_label
invoice_date The day the invoice was created
external_download_url Set when an external InvoiceRenderer owns the document
$invoice->lines;                  // HasMany InvoiceLine, ordered by sort_order
$invoice->hasLines();
$invoice->billable;
$invoice->invoiceable;            // the charge or single payment
$invoice->tenant;
$invoice->creditNote;             // the most recent credit note
$invoice->creditNotes;            // all of them
$invoice->isVoided();
$invoice->hasExternalDownload();

$charge->invoice;                 // MorphOne, from the payment side

Creating an invoice dispatches InvoiceGeneratedEvent; a credit note dispatches CreditNoteGeneratedEvent (Events). To mail the PDF with the subscriber mails, see notifications.attachments.

Invoice lines

Models\InvoiceLine is created together with the invoice and is immutable in the same way.

Property Description
invoice_id Owning invoice
key Origin cost-line key (module:agenda); null for base and single lines
description
quantity 1 for subscription lines; order lines carry their real quantity
unit_amount, amount Net of VAT — invoice subtotal semantics
vat_rate, vat_amount Per-line VAT, set for single-payment order lines; null = the invoice-level VAT is authoritative
period_start, period_end Billed period
sort_order Display order

Where the lines come from:

Amount semantics. Line amounts follow the invoice's subtotal (net of VAT):

This is the same per-row arithmetic the charge itself is composed with and the same split quoteFirstCharge() publishes before the customer pays, so a checkout screen and the invoice for one sale cannot name different numbers.

A charge with a single line — a first payment without cost lines, a legacy row, a single-amount payment — de-grosses the collected amount: there is no composition to follow, and the money that moved is the authority.

Numbering

Models\InvoiceSequence issues one continuous sequence per document type and per tenant: INV-000001, INV-000002, … and CN-000001 for credit notes. The prefix comes from invoicing.number_prefix (invoices) and invoicing.credit_note_prefix (credit notes). The number is zero-padded to six digits and grows past them on its own.

In Connect mode a tenant's documents carry the four-character reference code of its connected Mollie account in front (LCRA-INV-000001), and the tenant has its own sequence. The code stays after a disconnect, so a credit note for an earlier payment still carries it.

The sequence never resets — there is no year in the number. A per-year number like INV-2026-00842 tells every recipient how many invoices you sent this year, and with monthly subscriptions that is close to your customer count. The invoice date carries the year, which is all the law asks (art. 226 of Directive 2006/112/EC: a sequential number in one or more series that identifies the invoice uniquely).

For your bookkeeping this means a gap check per financial year runs on invoice_date, not on the number. Both are taken when the document is created, so a document that larsasub:generate-missing-invoices creates in January for a payment of December is a January document either way.

A fresh installation starts at INV-000001, which still says "first invoice". If that matters to you, seed the sequence once before the first invoice — see Starting the invoice sequence at an existing number.

A number is only consumed when a document is actually created, and a document cannot be deleted afterwards — which is why mandate-setup charges returning null matters: skipping them leaves no gap in the sequence.

VAT: what gets collected

invoicing.prices_include_vat is the single most consequential setting here, because it changes what is collected, not just what is printed. Both conversions below run only with invoicing.enabled on.

Setting price_amount means What gets debited
true (default, B2C) Gross — VAT is already inside it The listed amount. A zero-rated customer (EU reverse charge, outside the EU) pays the listed amount minus the VAT
false (B2B) Net — VAT is added before collection A €14,90 plan debits €18,03 domestically

Either way the invoice totals exactly what was settled: invoice.total equals the charge's pre-credit amount, and total - credit_applied equals what was actually debited. With exclusive prices the invoice splits that €18,03 back into €14,90 + €3,13. The conversion runs per breakdown row and the rows are summed, so the charge total is exactly the sum of its invoice lines.

Three cases are never grossed up:

Because the debit differs from the listed price in both directions, the subscriber mails that quote an amount — activation, started, renewal announcement, renewed, plan changed, "your subscription amount has changed" and "the VAT rate changed" — label their total row accordingly ("incl. 21% VAT", "VAT reverse-charged" or "0% VAT") and show it even when there are no cost lines. See Notifications.

VAT: the frozen snapshot

VAT is decided when the money moves, not when the invoice is written.

Every charge and single payment the package creates stores the rate it was actually collected at (vat_rate, 0.00 = zero-rated) and how the VAT relates to the collected amount (vat_treatment, an Enums\VatTreatment) — also while invoicing is off:

VatTreatment Meaning
Excluded No VAT is in the collected amount
Included The listed price already carried it — or the country was unknown and the package refused to gross up on a guess, or invoicing was off when the charge was created
Added The package grossed the amount up before collecting
Stripped No VAT is in the collected amount: the customer is zero-rated (reverse charge, outside the EU) on VAT-inclusive prices, and the package removed the VAT from the listed price before collecting

The invoice splits out Included and Added; Excluded and Stripped carry no VAT. A cost-line refund is valued from listed prices, so it re-adds the VAT only for Added and removes it only for Stripped — a later prices_include_vat switch cannot move the credit for days billed under the old mode. Test for "no VAT in this amount" with ! $treatment->includesVat(), never with === VatTreatment::Excluded: a zero-rated charge on inclusive prices is Stripped. Single payments and standalone charges are never Added or Stripped.

The charge also freezes the profile fields that decision rested on — country, type and VAT number, and with VAT-number validation on the check of that number (vat_check: status, consultation number, date) — in vat_customer, so the invoice states the customer the rate applied to. A reverse-charge line next to a profile that has since dropped its VAT number would be unjustifiable. Name and address still come from the current record.

The invoice follows that snapshot instead of re-resolving the plan rate and the country rules, because those inputs move: a plan rate can be edited, and invoice_country / invoice_type / invoice_vat_number are fields the customer changes themselves. An invoice generated days or months later therefore reports the VAT that was actually collected — and a charge created while invoicing was off is invoiced for what it collected when you switch invoicing on later.

Two kinds of row carry no snapshot: the €0.01 mandate-setup charge (never invoiced at all) and rows from before 0.14. Those resolve the rate and the country rules live at invoicing time.

vat_customer is encrypted. It is encrypted:array on a longText column (a ciphertext is not valid JSON, so a json column would be rejected by MySQL). Billable::anonymize() clears it, withTrashed() included — it is the only VAT column erasure touches. Rate, treatment and the reverse-charge flag are facts about the transaction, and vat_treatment is what a refund reads to decide whether it may re-add VAT, or has to remove it.

Reverse charge

The EU reverse charge applies to a customer in another member state than the seller (both in the EU) only when:

  1. the billable's invoice_type is Billable::INVOICE_TYPE_BUSINESS, and
  2. a VAT number is stored, and
  3. with VAT-number validation on: VIES confirmed that number (vat_check_status = valid). Off, the stored number is enough.

The seller country is invoicing.seller.country, or under Connect the tenant's getInvoiceSellerDetails()['country']. A domestic customer always pays VAT; a customer outside the EU pays none, without a reverse charge.

The profile type and the VAT number cannot disagree: Billable clears Billable::BUSINESS_ONLY_FIELDS (company name, VAT number, CoC number) on every model save where invoice_type is not business. A personal profile can therefore never carry a number into a reverse charge, which would produce an invoice with reverse_charge = true and no customer VAT number. A query-builder mass update skips that hook. See Billable models.

Whether a supplied VAT number is genuine is what Validating VAT numbers answers — opt-in; off, it stays yours. See Compliance.

Validating VAT numbers

Opt-in: invoicing.vat_validation.enabled (LARSASUB_VAT_VALIDATION_ENABLED), off by default. Off, the package makes no outgoing call for it. On, every business profile with a VAT number in an EU country is checked in VIES, the European Commission's register, and only a confirmed number is reverse charged.

When a number is checked

Domestic numbers are checked too: deciding "cross-border" needs the seller, which under Connect is the tenant. GB and Northern Ireland (XI) are not checked — outside the EU there is no reverse charge to justify.

The outcome

The outcome lives on the registration:

Column Meaning
vat_check_status An Enums\VatNumberStatus, below; null = no check applies, or never checked
vat_check_number The normalized number the status belongs to: the VIES country code plus the number without spaces, dots or its own prefix (NL123456789B01; Greece is EL123456789)
vat_check_reference The consultation number (VIES requestIdentifier) of the last valid answer — only issued when the check carried a requester
vat_checked_at When VIES last gave a definitive yes or no
VatNumberStatus Meaning
pending The number changed while the check is on; the queued check has not answered yet
valid VIES confirmed the number on vat_checked_at. A later check without an answer keeps it; only a definitive no or a change of the number replaces it
invalid VIES said no — the number does not exist, or VIES refused it as input
unavailable Never found valid, and the last attempt got no answer (a member state in maintenance, a VIES limit, a timeout, your own requester refused). Retried by the job, then twice a day

A status only counts for the number it was decided for. A query-builder mass update of invoice_vat_number skips the model hook that resets the check; the stale status then belongs to vat_check_number, not to the new number, and VatNumberValidationService::statusFor() answers null for it.

The requester is the seller's own VAT number, so the consultation is issued to whoever sells: invoicing.seller.vat_number in simple mode, the tenant's getInvoiceSellerDetails()['vat_number'] under Connect — never the platform's number for a tenant's sale. Without one VIES still answers, without a consultation number; boot and larsasub:doctor say so. VIES refusing your own requester (INVALID_REQUESTER_INFO) counts as no answer and is logged as an error.

What the check decides

With the check on, the reverse charge applies only to a number VIES confirmed. Until then — pending, invalid, unavailable or never checked — the business customer is charged VAT and reclaims it from their own tax office, and you carry no risk for a number nobody verified. Domestic customers and customers outside the EU are not affected: they pay VAT, or none, whatever VIES says.

Nothing is mailed to the customer about a refused number and the save is never blocked — listen to VatNumberCheckedEvent (Events) if you want either. VIES's name and address are not read or stored, and nothing is compared with them: that part of the verification stays yours (Compliance). The profile dashboard shows one line under the VAT number with the status (and the day it was verified).

Switching it on with existing customers

Do it in this order:

  1. Check the numbers you already have: php artisan larsasub:check-vat-numbers --unchecked runs while the feature is still off (and with the scheduler off).
  2. Then switch it on.

A number not checked by then loses its reverse charge until it is: the next charge of an existing reverse-charged customer carries VAT — on inclusive prices nothing is stripped any more, on exclusive prices the VAT is added. larsasub:doctor counts what is still unchecked.

In your own code

use PetersDevelopment\LarsaSub\Service\VatNumberValidationService;

$service = app(VatNumberValidationService::class);

$service->statusFor($billable);   // ?VatNumberStatus for the CURRENT number
$service->checkNow($billable);    // ask VIES now (up to `timeout` seconds) and return that status
$service->check($billable);       // ask VIES now; returns the raw VatNumberCheckResult, or null when no check applies
$service->checkDue();             // the scheduled sweep, when you run the scheduler yourself

checkNow() and check() do not look at invoicing.vat_validation.enabled — calling them is an explicit act — and do not set a tenant context: in your own request that is yours. Call them outside your own database transactions. checkDue(?int $limit = null, bool $uncheckedOnly = false) returns a report with the keys taken, valid, invalid, unavailable, skipped and errors.

To replace VIES — a cache in front of it, another source — implement Contracts\VatNumberValidator and name the class in invoicing.vat_validation.validator:

use PetersDevelopment\LarsaSub\Contracts\VatNumberValidator;
use PetersDevelopment\LarsaSub\Support\VatNumberCheck;
use PetersDevelopment\LarsaSub\Support\VatNumberCheckResult;

class CachedVatNumberValidator implements VatNumberValidator
{
    public function check(VatNumberCheck $check): VatNumberCheckResult
    {
        // $check->country is the VIES code (EL for Greece), $check->number the
        // national part; $check->requesterCountry / ->requesterNumber are the
        // seller, or null without one.

        return VatNumberCheckResult::unavailable();   // or ::valid(...) / ::invalid(...)
    }
}

"No answer" must be VatNumberCheckResult::unavailable(); anything thrown counts as that.

Configuration

Key Default Meaning
invoicing.vat_validation.enabled false The switch (LARSASUB_VAT_VALIDATION_ENABLED)
invoicing.vat_validation.validator null A Contracts\VatNumberValidator class; null = the bundled VIES client
invoicing.vat_validation.recheck_after_days 30 Days after which a valid number is checked again; null = only on change (LARSASUB_VAT_RECHECK_DAYS)
invoicing.vat_validation.timeout 10 Seconds one VIES call may take
scheduling.check_vat_numbers_frequency twiceDaily Schedule of larsasub:check-vat-numbers
scheduling.check_vat_numbers_delay_ms 500 Pause between two VIES calls in the sweep
scheduling.check_vat_numbers_limit null How many registrations one run takes up; empty = all (also --limit=)

Boot refuses a validator class that does not implement the contract and a recheck_after_days that is neither null nor a positive whole number. It warns when the check is on without invoicing (the check then changes labels, not amounts), or without a seller VAT number in simple mode.

Per-subscription VAT rate

Subscription and BillableSubscription both carry a nullable vat_rate. The rate a subscription charge is grossed up or stripped with, and the one its snapshot freezes, is resolved when the charge is composed, on its collection day:

BillableSubscription.vat_rate (an override) → Subscription.vat_rate (now) → invoicing.vat_rate (now)

The rate is not copied onto a subscription when it is created. A period is paid in advance, and VAT on a payment on account is due at the rate in force on receipt (Directive 2006/112/EC, art. 65 and 93), so a legal rate change, or a corrected classification on the plan, reaches every running subscriber from their next charge. The price stays frozen: that is a contract term. A charge that exists keeps the rate it was composed at, and an invoice is built from the charge's snapshot — paid charges never move.

$billableSubscription->getEffectiveVatRate();  // null = the config rate applies

When a plan's own rate changes through the model, SubscriptionVatRateChangedEvent fires and the subscribers without a rate of their own whose debit changes — on exclusive prices a customer who pays VAT, and a zero-rated customer on inclusive prices — get a mail with the new amount (notifications.user.subscription_vat_rate_changed, on by default). There is no lead time: the next charge uses the new rate. A change of invoicing.vat_rate in the configuration is not observable and sends nothing; see Compliance.

Credit notes

A refund or chargeback voids invoiced value through a credit note, and for everything the provider reports the package writes it for you: each completed refund, each chargeback, and credit given back from a paid charge with CreditService::returnAppliedCredit() produces a note for exactly that amount, settled against one cumulative per invoice. A refund you request with RefundService::refund() is covered the same way once it completes — see Refunds. The invoice becomes voided when the notes reach its total; a partial note leaves it finalized.

Call createCreditNote() yourself only for a correction outside a provider refund:

$creditNote = app(InvoiceService::class)->createCreditNote($invoice, 'Correction #1234', 12.50);

createCreditNote(Invoice $invoice, string $reason, ?float $creditedCumulative = null) returns null when there is nothing left to credit. $creditedCumulative is the cumulative credited amount after this note, measured against every note on the invoice — the automatic ones included — so a series of partial credits cannot over-credit it. Leaving it null credits the whole remaining invoice.

Mixing the two is where money disappears from the books, in both directions:

So: correct an invoice that may still see a provider refund by refunding through the provider, not with a manual note.

Models\CreditNote (larsasub_credit_notes) is numbered from its own sequence and is immutable like the invoice. It references the invoice it credits (invoice_id, $creditNote->invoice) and stores its own total, vat_amount, vat_rate, reverse_charge, currency, reason and issued_at; seller and customer details are the invoice's. Its VAT is the invoice's VAT in proportion to the cumulative credited amount, so the notes' VAT adds up to the invoice's VAT exactly once the invoice is fully credited.

Downloading

The bundled dashboards expose endpoints that stream the PDF. The two payments endpoints generate the invoice first when it does not exist yet (idempotently):

Route name URL
larsasub.my-payments.charge-invoice /billing/payments/charges/{charge}/invoice
larsasub.my-payments.single-payment-invoice /billing/payments/single-payments/{singlePayment}/invoice
larsasub.my-invoices.download /billing/invoices/invoices/{invoice}/download
larsasub.my-invoices.download-credit-note /billing/invoices/credit-notes/{creditNote}/download
larsasub.tenant-invoices.download /billing/admin/invoices/invoices/{invoice}/download
larsasub.tenant-invoices.download-credit-note /billing/admin/invoices/credit-notes/{creditNote}/download

Guards: the ownership policy, and invoicing must be enabled (404); the two payments endpoints also require a paid payment (422). The invoices dashboards — and so their routes — are only registered with invoicing on. The tenant routes serve only documents of the current tenant (simple mode: the operator sees all). The charge and single-payment API resources expose invoice_url (null when not applicable), and the payments dashboard renders an "Invoice (PDF)" button for paid rows.

Programmatically:

$service->downloadInvoice($invoice);       // StreamedResponse, or a redirect to an external URL
$service->downloadCreditNote($creditNote);

Country codes and names

Helpers\CountryHelpers owns the country data:

use PetersDevelopment\LarsaSub\Helpers\CountryHelpers;

CountryHelpers::normalize('  nl ');   // 'NL'  ('' becomes null)
CountryHelpers::isEu('NL');           // true — the 27 member states
CountryHelpers::name('NL');           // 'Netherlands' — 'Nederland' under nl
CountryHelpers::nameOrCode('XX');     // 'XX'
CountryHelpers::display('NL');        // per invoicing.country_display
CountryHelpers::options();            // for a form select
CountryHelpers::euOptions();
CountryHelpers::isKnown('NL');

The code is the source of truth. Every VAT comparison runs on normalized codes, never on names. The bundled names come from a committed ISO 3166-1 list generated from ICU, so a code yields the same name across servers and ICU versions.

The list carries current codes only — deprecated aliases (DD, FX, SU) are excluded on purpose: they are selectable duplicates that isEu() does not know, so picking one would zero-rate an EU customer.

invoicing.country_display decides how invoices and credit notes print a country: 'name' (default, Netherlands) or 'code' (NL). Unknown codes always fall back to the code — an invoice must never lose information because a name is missing.

Names ship in en and nl: English from the bundled list, Dutch as larsasub::countries.{CODE} translation keys covering the same 252 codes. The name follows the locale the document renders in. For another language, or different wording, define larsasub::countries.{CODE} in your own app; that overrides the package's name.

The built-in PDFs

The package renders invoices and credit notes with dompdf, from two Blade views. This section covers their language, how amounts are laid out, and how to change the views.

The document's language

The document's language is the customer's. The built-in invoice and credit-note PDFs render in the billable's content locale — the language their frozen lines are written in — whoever opens them, so the attachment in the customer's mail and a download by an operator are the same document. Amounts use the separators of the package's two UI languages — 1.234,56 for nl, 1,234.56 for en and every other locale — beside the currency code, and dates are written out (5 October 2026 / 5 oktober 2026).

That locale is larsasub_billables.locale when one is stored (your owner model's billable_locale), else locales.default, else the first of locales.supported. On a single-language install with none of those set it falls back to app.locale — and Laravel changes that value whenever a request switches locale, so the document then follows whoever opens it. Set larsasub.locales.default (or store a locale per billable) to pin it.

Price display

invoicing.price_display is presentation only; stored invoice data is always net + VAT.

Value Layout
'exclusive' (default) Line amounts ex VAT; subtotal + VAT + total (B2B style)
'inclusive' Line amounts incl VAT; total first, then "of which VAT" per rate group, with its taxable base (B2C style)

Mixed-rate invoices get one "of which VAT" row per rate; single-rate invoices show the stored invoice-level amounts, with no rounding re-derivation. In the inclusive layout the per-line gross amounts are made to add up to the stored total: a rounding remainder of at most a cent per line lands on the last line.

When credit was applied, both layouts print it below the total, followed by the amount actually due (total - credit_applied). A reverse-charged invoice carries the reverse-charge statement.

Customizing the PDF views

Publish the two views alone:

php artisan vendor:publish --tag=larsasub-invoice-views

They land in resources/views/vendor/LarsaSub/pdf (invoice.blade.php, credit-note.blade.php). A published view is frozen at the version you published: re-publish with --force after an upgrade that changes them, and re-apply your changes.

Keep the views safe for dompdf: print data escaped ({{ }}, or {!! nl2br(e(...)) !!} for line breaks) and load no remote assets — embed images and fonts locally.

Using your own invoicing system

Bind your own Contracts\InvoiceRenderer. LarsaSub still creates the invoice and credit-note records (numbers, lines, VAT split); your renderer decides where the document is downloaded:

namespace App\Billing;

use PetersDevelopment\LarsaSub\Contracts\InvoiceRenderer;
use PetersDevelopment\LarsaSub\Models\CreditNote;
use PetersDevelopment\LarsaSub\Models\Invoice;

class ExternalInvoiceRenderer implements InvoiceRenderer
{
    public function getDownloadUrl(Invoice $invoice): ?string
    {
        // Push $invoice (and $invoice->lines) to your own system and return
        // its download URL — or null to use LarsaSub's own PDF.
        return app(MyAccountingClient::class)->publishInvoice($invoice);
    }

    public function getCreditNoteDownloadUrl(CreditNote $creditNote): ?string
    {
        return app(MyAccountingClient::class)->publishCreditNote($creditNote);
    }
}
// A service provider
$this->app->bind(InvoiceRenderer::class, ExternalInvoiceRenderer::class);

MyAccountingClient stands for your own integration. The renderer is called once per document, right after the record is committed, and the URL it returns is stored in external_download_url; the download endpoints and downloadInvoice() / downloadCreditNote() then redirect there. Returning null falls back to LarsaSub's own PDF. A renderer that throws is logged and the document falls back to the package PDF — invoice generation never fails on it.

With your own renderer bound and always returning a URL, dompdf is not needed at all; without dompdf, the built-in renderer throws a LarsaSubException naming the missing package rather than a class-not-found mid-stream.