Skip to content

Invoices 1.1.0 — Production Readiness Audit

FieldValue
Date2026-08-15
StatusGo — staging → production after CI, migrate, and scheduler confirm
ScopeTenant Invoices module invoices 1.0.0 → 1.1.0 (recurring series + PDF)
Branchfeature/recurring-invoices-pdf
CompanionInvoices production · Developer · User · API

This is an additive MINOR on the existing customer-invoice module (not Stripe/Cashier, not Central Invoice). No new permissions or queues. Optional INVOICES_* env vars have production-safe defaults.


Executive summary

Workspaces that already entitled Invoices pick up recurring + PDF after migrate. Catalog bump does not auto-install for workspaces that never entitled the module.

GateResult
module:invoices + invoices.* (no new family)Pass
PDF invoices.view + assignee scope + throttle:invoices-pdfPass
Stop series invoices.update; optional void invoices.voidPass
Workspace timezone due generationPass
Idempotent daily generate + unique (tenant, source, issue_date)Pass
Soft-deleted occurrence does not unique-fail the tenant runPass (F1)
Generator chunkById + per-tenant time budgetPass (F2)
PDF cache + warm job on send (default queue)Pass (F3)
Catch-up cap 52 + remaining periods next tickPass (F4)
Command FAILURE if any tenant series failsPass (F5)
Invoice memo on Overview + PDFPass (F6)
Headed e2e stop with void_latest_unpaidPass (F7)
Dompdf isRemoteEnabled=false, Blade escapedPass
Catalog 1.1.0 migrate-onlyPass
Pest recurrencePass
Playwright headed one-sessionPass (5)
Docs + upgrade + runbookPass

Go / No-Go: Go after companion CI, composer install, php artisan migrate --force, and confirming invoices:generate-recurring is on the production scheduler.


Findings

IDSeverityStatusFindingAction
F1MediumFixedalreadyIssued ignored soft-deleted children, so retrying the same issue_date could hit the unique index and abort that tenant’s remaining serieswithTrashed() + per-series try/catch; Pest
F2MediumFixedGenerator get()s all due series rootschunkById (INVOICES_RECURRING_CHUNK_SIZE, default 100) + per-tenant time budget
F3MediumFixedPDF is synchronous Dompdf on the requestCache by id + updated_at; WarmCustomerInvoicePdfJob on send (default queue); throttle:invoices-pdf
F4LowFixedCatch-up cap was 12 periods per runDefault 52; remaining due periods run on the next daily tick; Pest cap=1 then two artisan runs
F5LowFixedCommand logged tenant_failed and still exited 0Returns FAILURE if any entitled tenant had a failed series or exception
F6LowFixedInvoice memo (notes) was on the PDF onlyOverview shows the memo; Notes tab remains the activity stream
F7LowFixedHeaded e2e stopped a series without void_latest_unpaidGenerate a child from Playwright (tenant-scoped tinker), then check the void checkbox

Accepted / intentional

ItemNotes
No new permissionsReuse invoices.view (PDF), invoices.update (stop), invoices.void (optional child void)
Generated invoices are DraftOperator sends; recurring generator does not auto-email
Stopping does not void paid historyOptional checkbox only voids latest unpaid generated draft/sent
No stored pdf_pathRendered on the fly (cached)
No new queue nameWarm job uses the default Redis queue
Customer email deliveryShipped in 1.8.0POST …/email (invoices.send, throttle:billing-document-email); see API and User guide

Test evidence

SuiteResultNotes
php artisan test --compact tests/Feature/Tenant/CustomerInvoice/CustomerInvoiceRecurrenceTest.php13 passedActivate on send, frequency required, generate + idempotent, timezone, stop, stop+void, reject stop on child, PDF + cache hit, PDF 403, catch-up cap, soft-delete skip, schedule registration, command failure exit
npm run test:e2e:invoices:headed5 passed (~7.5m with headed slowMo)One login session

Headed e2e (single session): empty/title-only validation; all five frequencies; create with tax totals; lines/Overview memo/timeline; edit; PDF from sheet and row menu; send; Sent KPI; void; recurring generate + stop with optional void; n / Ctrl+F; delete/restore; My invoices filter.


Deploy order

  1. Backendcomposer install (pulls dompdf/dompdf) then php artisan migrate --force (recurrence columns + catalog 1.1.0)
  2. Confirm scheduler: invoices:generate-recurring daily, withoutOverlapping(120), onOneServer
  3. Optional env only if you need to tune catch-up / PDF (see Invoices production)
  4. SPA — recurring form, stop dialog, Download PDF, Overview memo
  5. Docs
  6. Staging smoke below before production traffic

Suggested merge: Backend → Frontend → Docs. Do not db:seed.


Pre-flight checklist

#CheckOwnerPass?
1composer install includes dompdf/dompdfOps
2Migrations applied (add_recurrence_to_customer_invoices + bump 1.1.0)Ops
3Catalog invoices version 1.1.0Ops
4Scheduler has invoices:generate-recurringOps
5Pest recurrence + existing CustomerInvoiceTest green in CIEng
6Playwright test:e2e:invoices greenQA
7Staging smoke signed offQA / Ops

Staging smoke (human)

  1. Marketplace → Invoices already installed (or install free)
  2. New invoice → empty submit shows Title + Description required
  3. Recurring on → frequencies Weekly / Monthly / Quarterly / Semi-annually / Yearly
  4. Create draft with a line and a memo → Overview shows the memo → Download PDF (opens a .pdf)
  5. Send recurring invoice → badge Recurring, Next invoice, Stop recurring
  6. php artisan invoices:generate-recurring → a Draft child appears for the next period
  7. Stop recurring and check Also void the latest unpaid generated invoice
  8. Void a sent non-recurring invoice
  9. Soft-delete → Deleted only → Restore

Rollback

LayerAction
FrontendRedeploy previous SPA (recurring/PDF UI disappears; API columns remain)
Backend codeRedeploy previous release; keep additive migrations
Module disableMarketplace uninstall (invoice rows retained)
SchemaDo not roll back recurrence columns in prod without a data plan
SchedulerRemoving the command stops new drafts; existing series rows stay

Monitoring

  • Domain timeline: recurrence_started, recurrence_stopped, recurrence_generated
  • Logs: invoices.generate-recurring.tenant_failed, invoices.generate-recurring.series_failed, invoices.generate-recurring.time_budget_reached, invoices.pdf.rendered, invoices.pdf.warm_failed
  • Nightwatch: invoices:generate-recurring duration and non-zero exit; GET invoices/{id}/pdf latency
  • Spatie log name customer_invoices

Sign-off

RoleNameDateDecision
EngineeringGo / No-Go
ProductF1–F7 fixed
OpsStaging migrate + scheduler + smoke ☐

Recommendation: Merge companions after CI green; run staging smoke.

Official documentation for the EloSync SaaS Platform.