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...
Package role and release scope. v2.8.0 is a brand-context correctness release. It fixes a genuine defect: on multi-brand hosts, console and scheduled runs read the package's bundled defaults instead of the settings saved on the settings page, and invoices:pending wrote invoices with an empty seller rather than the brand's. For a single-brand install the release content is effectively a bug fix. For a multi-brand host it changes three things at once — how many times a command runs, where its configuration comes from, and what files it produces. The version bump to a minor is defensible for single-brand and misleading for multi-brand; the changelog should carry an explicit upgrade section, and multi-brand operators should plan it as a behavior-change release rather than a patch.
Architecture fit.
--brand=<handle or id> is the necessary escape hatch: it enables targeted reruns and lets operators shard schedules per brand across nodes.invoices:held and invoices:brand-check cross-brand is a good decision — it preserves the global support queue that the v2.7.0 runbook depends on.invoices:export pdf --queue) closes a real correctness hole; previously the worker would render with default-brand settings.send() on a failed row bypasses the atomic claim, so duplicate sends remain possible regardless of this release.Integration feasibility. Low code effort, materially higher verification effort than v2.7.0. No migration is described in the release notes — confirm. Budget roughly 0.5 dev-day for single-brand and 1.5–2 dev-days for multi-brand, dominated by inventorying export consumers, auditing historical invoices:pending records, and per-brand smoke tests. Multi-brand hosts with automated ingestion of export files have a coordination dependency outside engineering that can dominate the calendar.
Technical risk.
| Risk | Severity | Notes |
|---|---|---|
| CLI/scheduled runs now read saved settings, not env/config | High (multi-brand) | The v2.7.0 kill switch (INVOICES_RETRY_SCHEDULE=false) may no longer disable retries. Retry schedule, attempt count, tax and text behavior can now change via the UI with no deploy and no code review. Verify precedence before relying on config for rollback. |
| Export artifact change | High where ingestion is automated | One file per brand; belege.csv becomes belege_marke-2.csv; CSV to stdout requires --brand. A consumer that globs the old filename will silently ingest one brand or none. |
| Historical invoices written with empty seller | Medium-High | Invoices created by invoices:pending on multi-brand hosts before 2.8.0 carry the package's empty seller. This is a document-correctness issue, not just cosmetic; audit and backfill. |
| Per-brand retry policy drift | Medium | Retry timing is now per-brand data. Staging no longer proves production, and a settings edit is a live behavior change with no deployment record. |
| Coarse exit codes / silent partial failure | Medium | A failing brand does not stop the loop; the exit code is only the first failure. Cron alerting on exit code alone cannot identify which brand failed. |
| Queued job payload change | Medium | Jobs queued before deploy lack brand context and will run with default-brand settings. Drain queues before rollout. |
| Command runtime and overlap | Medium | Per-brand loops multiply runtime by brand count; long invoices:export runs can collide with withoutOverlapping where they previously fit comfortably. |
brand_id 0 / orphaned brands |
Low-Medium | release maps brand_id 0 to the default brand; export reports unknown brands and exits 1. Confirm pending and retry behavior for these rows — writing under the default brand could mis-attribute documents. |
--brand accepts handle or id |
Low | Typo and ambiguity risk; confirm invalid values fail fast with non-zero exit rather than no-op (a silent no-op in cron is a missed run). |
| Semver communication | Low-Medium | For multi-brand, this is an output-format and behavior break. Request a "Behavior changes" heading and a documented filename scheme. |
| v2.7.0 risks carried forward | — | Scheduler absent, migration skipped, duplicate send(), invoices:release guard, backoff timing vs SLA, no jitter. All unchanged. |
Key questions for the vendor.
INVOICES_RETRY_SCHEDULE=false still authoritative?invoice_deliveries migration run first?--brand value — fail fast with non-zero exit, or no-op? A no-op in a cron entry is a silent outage.--json, per-brand exit summary), and do log lines include brand handle/id so alerting can group by brand without parsing prose?--brand plus --output produce exactly (suffix or exact path)?invoices:pending on multi-brand hosts before 2.8.0: what was actually written for seller/brand, and is there a supported audit or backfill command?pending, retry, and release do with brand_id 0 and orphaned brand references — consistent with export's report-and-exit-1?invoices:retry --brand=X safely run for different brands in parallel on multiple nodes with onOneServer?invoices:retry re-read the brand's retry settings on every run, and does raising max_attempts mid-flight reopen rows already marked failed_final?invoices:release now uses the invoice's brand, so subject and filename can differ from what the same invoice produced earlier. Confirm this is strictly the saved template with no cached rendering, and that brand_id 0 → default is intentional for document identity.Stack fit. Assumptions are unchanged from v2.7.0: database-backed deliveries, configured mail transport, cron-driven schedule:run, no queue worker required. The new variable is brand dimensionality. Multi-brand hosts must choose an operating model: one scheduler entry per host looping all brands, or one entry per brand using --brand. The first is simpler, the second gives per-brand isolation and bounded runtimes. Single-brand installs genuinely need nothing beyond a normal patch cycle.
Migration path.
invoices:pending on multi-brand hosts, inspect seller and brand_id, and decide whether remediation happens before or after rollout.invoices:export: globs, SFTP drops, BI imports, filenames expected, and anything reading CSV from stdout (now requires --brand). This is the critical path if automated ingestion exists.2.8.0 on staging; confirm no migration ships; run each affected command with and without --brand; capture exit codes, filenames, and log formatting.Compatibility and rollback. Code rollback is structurally safe — there is no schema change to unwind — and it restores package-config sourcing and the single combined export file. Two caveats. First, artifacts already produced under the new scheme persist (per-brand files, corrected sellers), so consumers must tolerate a mixed window. Second, if retry behavior is now settings-driven, rollback restores env behavior; the kill switch must be re-tested after rollback, not assumed. Given the settings/source ambiguity, roll forward is usually the better path; decide the rollback trigger before deploy rather than after.
Sequencing recommendation. Keep 2.8.0 in its own release train, separate from v2.7.0's scheduler-enablement gate. Suggested order: (a) 2.8.0 with retries still disabled — verify brand correctness of pending, export, and release; (b) re-verify the retry kill switch and then enable retries per brand; (c) app-caller audit for stray send() calls. Bundling 2.8.0 with retry enablement is the specific mistake to avoid: during an incident you would not be able to tell whether brand settings or the claim logic caused it.
Maintenance. Two new surfaces need owners. First, the settings page becomes production configuration: changes there alter scheduled behavior immediately, so it needs change control, a review step, and freeze windows around releases. Second, export filename stability becomes a cross-team contract with finance or BI, not an internal detail. Also confirm that archived or empty brands are skipped cleanly so the per-brand loop does not generate spurious runs and alerts. Status enumerations (failed_final from v2.7.0) now need brand attribution in dashboards and filters.
Support. Runbook changes before go-live:
invoices:held remains global, which is correct. Confirm the output shows brand so support can route the ticket without a second query.held, then read the invoice's brand_id before releasing. invoices:release now sends under the invoice's own brand, so subject line and attachment name may differ from the previous send — that is expected, not a regression.--brand=<suspected> to isolate.Alerting should combine: the v2.7.0 error-level line for failed_final, a per-brand count of failed_final rows older than one hour, and per-brand export freshness (expected filename exists and is recent). The freshness check is what catches the "second brand's file never appeared" case that exit codes and file-presence-any checks both miss.
Scaling.
--brand schedules across nodes with onOneServer so a single run does not monopolize a scheduler tick.Failure modes.
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Settings edited in UI break CLI behavior | Retries stop, accelerate, or render wrong templates with no deploy | Diff saved settings against expected; per-brand failed_final count |
Revert setting; add change control and a settings snapshot |
| Env kill switch no longer effective | Disabling retry via env has no effect | Test the disable path each release; watch retry attempts after "disabling" | Use the actual supported switch; get vendor confirmation |
| Export filename change breaks ingestion | Accounting or BI import stops, or only one brand is ingested | Per-brand file freshness alert | Update consumer to per-brand globs, or run with --brand to fixed names |
| One brand fails inside the loop | Some brands succeed, one silently does not; exit 1 only | Per-brand artifact freshness; brand-tagged logs | Rerun --brand=<failed> |
| In-flight queued export lacks brand | Documents render with default-brand settings | Spot-check PDFs post-deploy | Drain queue pre-deploy; re-queue affected jobs |
| Historical invoices with empty seller | Blank or wrong seller on issued documents | Data audit on pre-2.8.0 rows | Backfill and reissue where legally required |
| Cron absent or misconfigured (carried from 2.7.0) | failed rows accumulate, never advance |
Row-age alert on failed |
Fix scheduler; invoices:retry is idempotent |
App calls send() on a failed row (carried) |
Duplicate mail | Customer complaint | Caller audit; enforce a single send path |
Release guard on young sending rows (carried) |
Operator blocked, support ticket | Ticket pattern | Wait out the window or lower delivery.release_sending_after_minutes with the duplicate-send tradeoff understood |
| Migration skipped (carried) | Failed sends stuck in sending |
sending rows older than 10 minutes |
Run migration; invoices:release to unstick |
Ramp-up. Single-brand: unchanged from the prior assessment — about half a day of dev including the forced-failure test, 2–3 hours of ops, a 30-minute support briefing, roughly one week to confident operation. Multi-brand: add 1–2 dev-days for the brand audit, export-consumer inventory, and per-brand smoke tests, plus calendar time for the finance/BI filename coordination and for standing up settings change control. Realistic elapsed time is two to three weeks, with the export-consumer update as the critical path whenever automated ingestion exists. The retry path remains easy to deploy and easy to leave unverified; combined with settings-driven behavior, an unverified multi-brand rollout is worse than doing nothing, because it will look correct in one brand and be quietly broken in another.
How can I help you explore Laravel packages today?