Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Statamic Invoices Laravel Package

goldnead/statamic-invoices

A Statamic addon for creating and managing invoices in your control panel. Define invoice data and line items, render branded templates, and keep billing documents alongside your content. A lightweight option for Laravel and Statamic sites needing simple invoicing without an external billing platfor...

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Upgrade and migrate first. v2.7.0 added attempts and last_error to invoice_deliveries; if you're jumping from anything older, run php artisan migrate before deploying code that relies on retries. There is no schema change in v2.8.0, but the columns are still the prerequisite for a failed send to be recordable at all.
  2. Know which settings your console runs use. As of v2.8.0, invoices:pending, invoices:retry, invoices:recheck-vat-ids and invoices:export no longer read the package config when running from the console — on a multi-brand host they run once per brand, on that brand's rows, with that brand's saved settings (the settings page). Single-brand installs behave as before. This is the single biggest day-to-day change: the settings page is now the source of truth for scheduled behaviour.
  3. Make sure the scheduler is actually running. The package registers invoices:retry every five minutes. Nothing is retried unless php artisan schedule:run fires every minute on the host (cron or schedule:work). On a multi-brand host one scheduled entry now fans out across brands.
  4. Look first at the delivery rows, then the commands. invoice_deliveries (state, attempts, last_error) is the source of truth; invoices:held is the quick human-readable view and is deliberately cross-brand, so it's still your first stop before narrowing with --brand.
  5. First use case: a send fails (renderer or mail server), you do nothing, and the package retries it up to delivery.retry.max_attempts (default 3) with a doubling gap starting at delivery.retry.after_minutes (default 10 → 10, 20, 40 minutes). After the last attempt the row becomes failed_final and is logged at error.
php artisan migrate                    # only if coming from < 2.7.0
php artisan invoices:held              # cross-brand: what is stuck / exhausted
php artisan invoices:retry --brand=acme   # narrow a run to one brand
php artisan invoices:release {invoice}    # manual re-send, under the invoice's own brand

Implementation Patterns

Let the package own retries. Don't wrap send() in your own retry() helper or queue-level retry loop. Rows are claimed atomically and only failed rows are retried, which is what preserves the exactly-one-mail-per-invoice guarantee. Duplicating retry logic outside the package is how you send the same invoice twice.

Tune the backoff deliberately — and in the right place. With defaults, the three attempts land at roughly +10, +30 and +70 minutes, and the five-minute schedule adds up to five minutes of granularity to each. For transactional invoices that's fine; if you need a longer window, raise delivery.retry.max_attempts rather than shortening after_minutes — the gap doubles per attempt. On multi-brand hosts the retry command reads each brand's saved delivery settings, so tune it on the settings page per brand; the config file remains the default/fallback and is what single-brand installs use.

// config/invoices.php  (defaults; per-brand saved settings win on multi-brand hosts)
'delivery' => [
    'retry' => [
        'max_attempts'  => env('INVOICES_RETRY_MAX_ATTEMPTS', 3),
        'after_minutes' => 10,
        'schedule'      => env('INVOICES_RETRY_SCHEDULE', true),
    ],
    'release_sending_after_minutes' => 10,
],

Self-scheduling sites: if you already drive commands from routes/console.php or the console kernel, set INVOICES_RETRY_SCHEDULE=false so the addon doesn't register a second entry, then schedule it yourself. Your own entry still gets the per-brand fan-out — that behaviour lives in the command, not the schedule registration.

Schedule::command('invoices:retry')->everyFiveMinutes();
// or, to keep the scheduled run predictable and cheap:
Schedule::command('invoices:retry --brand=acme')->everyFiveMinutes();

Treat --brand as a triage tool, not the default. The fan-out is what makes multi-brand hosts correct; using --brand everywhere silently drops a brand from the nightly export or the retry sweep. Use it for manual intervention, backfills, and re-running after fixing one brand.

Build a failed_final runbook. failed_final rows are never retried automatically; invoices:release is the only path back. Alert on the log line invoices: delivery failed for good, out of attempts (grep-able, stable) or on invoice_deliveries.state = 'failed_final', and have support run invoices:release <invoice> after fixing the cause. Since v2.8.0 the release sends under the invoice's own brand (subject, file name), and an invoice with brand_id 0 goes out with the default brand — verify that's the branding you actually want before the first manual run.

InvoiceDelivery::where('state', 'failed_final')
    ->where('updated_at', '>', now()->subDay())
    ->get(['invoice_id', 'brand_id', 'attempts', 'last_error']);

Debug with attempts / last_error. Before escalating, read the stored error on the row — it distinguishes a renderer failure (template/blade/data problem, retries won't help) from a transport failure (mail server, retries will help). Fix the renderer issue, then release manually instead of burning attempts.

Export with brands in mind. On a multi-brand host, invoices:export without --brand produces one export per brand; with --output each brand gets its own file (belege.csv → belege_marke-2.csv); CSV to standard output requires --brand. If your accounting pipeline consumes the export, decide up front between one file per brand or one explicit --brand run per brand, and update the consumer's filename expectations in the same deploy.

php artisan invoices:export --output=storage/exports/belege.csv      # one file per brand, suffixed
php artisan invoices:export --brand=acme > belege.csv                # single-brand to stdout

Queued exports carry the brand. invoices:export pdf --queue enqueues jobs that carry the brand and see the saved settings in the worker. That means the worker must be able to resolve the same brands and settings — if your queue workers run from a different config or database connection, you'll get package-default output rather than the brand's.

Greeting data. buyer.name is the stored name, falling back to the local part of the address; buyer.email is always the full address. There is no user/customer lookup — an invoice stores no link to an account. If you want a real salutation, populate the stored name at invoice creation time; don't try to reverse-engineer it from the email.

Tests. Keep Mail::fake() assertions to one mail per invoice and add a case that forces a transport exception, then asserts the row is failed with attempts = 1 and a populated last_error (not deleted, not sent). Add multi-brand coverage: a command run that iterates two brands, a --brand run that touches only its own rows, a brand_id = 0 invoice, and the export filename suffix.

Gotchas and Tips

  • Console commands now use saved settings, not config. If your retry cadence or VAT settings look wrong in a scheduled run but right in config/invoices.php, check the settings page — config:clear won't help, because the value lives in the database. Conversely, if you expected config to win on a multi-brand host, you've just lost an env-based override.
  • Multi-brand export filenames changed. belege.csv becomes belege_marke-2.csv (one per brand). Anything downstream that assumed a single fixed filename — ingestion scripts, SFTP pushes, dashboards — breaks in a way that looks like "no data" rather than an error.
  • CSV to standard output now needs --brand. Scripts that piped invoices:export to stdout will start failing on multi-brand hosts. Adding --brand is the fix; the fan-out alternative is only available with --output.
  • New failure mode: orphan brand_id. Documents whose brand_id names no brand are reported and the exit code is 1. They will never export until you either restore the brand or reassign the rows — schedule a periodic query for them rather than discovering it during month-end.
  • Exit code semantics are "first failure", not "all clear". A failing brand doesn't stop the next one, so you can have a partial success with exit code 1 — and a run that's 90% done and reported as a plain failure. Don't auto-retry the whole fan-out on a non-zero exit; re-run with --brand for the one that failed.
  • invoices:brand-check and invoices:held stay cross-brand on purpose. --brand doesn't narrow them. Don't build dashboards that assume otherwise, and don't be surprised when held shows other brands' rows.
  • invoices:release now brands the mail. Subject and file name come from the invoice's brand; brand_id 0 falls back to the default brand. Changing the default brand changes the appearance of previously-stuck invoices on release — expect a diff in support screenshots.
  • Skipping the migration is still the loudest failure mode (for installs below 2.7.0). Without attempts / last_error, a failed send can't be recorded and sits in sending forever, untouched by invoices:retry, until someone runs invoices:release. Deploy migration and code together.
  • No scheduler, no retries — silently. There is no warning that schedule:run isn't firing; rows simply stay failed. Verify with schedule:list after deploy, especially if you also flipped INVOICES_RETRY_SCHEDULE.
  • invoices:release will refuse a sending row younger than 10 minutes (delivery.release_sending_after_minutes). That's a guard against double-sending an in-flight mail, not a bug. If you know the send is dead, wait it out; if you routinely hit this, your sender timeouts are longer than 10 minutes — raise the config to match.
  • Behaviour change in failure handling (2.7.0): a failed attempt now keeps its row as failed (it used to remove the claim). Anything that counted deliveries by row absence, or treated "no row" as "not attempted", needs revisiting.
  • send() after a failure still sends. A second explicit call behaves as before, so idempotency across a manual call plus an automatic retry is only protected by the state machine — don't call send() from a job that also retries.
  • Greeting text change can break template tests. buyer.name is never the full address; assertions expecting [email protected] in the salutation will fail. Expect jane when no name is stored. Cosmetic in production, noisy in CI.
  • Config cache still applies where config is what's read. After changing INVOICES_RETRY_* env values run php artisan config:clear (or re-cache) — but on multi-brand hosts also re-check the settings page, since that's what the commands actually consume.
  • Overlapping runs are survivable thanks to atomic claiming — including a fan-out scheduled run overlapping a manual --brand run — but don't schedule invoices:retry more frequently than the documented five minutes: extra runs add log noise and lock contention without improving latency.
  • Log level matters. The terminal failure is logged at error; if your production log level is critical or you ship logs elsewhere, add an explicit alert on failed_final rows instead of relying on log scraping.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk