Invoice facade (D6-04). Owns lazy PDF rendering + storage delegation.
v1.0 persists ZERO PDF bytes — every render_invoice_pdf/2 call
re-hydrates the invoice from the current DB state + current branding
snapshot (roadmap SC #2 — retroactive brand consistency). Hosts that
need to persist a PDF MUST call store_invoice_pdf/2 explicitly; it
is never implicit.
Contract
render_invoice_pdf/2— returns{:ok, binary}on success,{:error, %Accrue.Error.PdfDisabled{}}when the configured adapter isAccrue.PDF.Null, and{:error, :chromic_pdf_not_started}when the configured adapter isAccrue.PDF.ChromicPDFbut the ChromicPDF GenServer is not running in the host supervision tree (D6-04 safety net — Pitfall 4).store_invoice_pdf/2— renders viarender_invoice_pdf/2and writes the binary toAccrue.Storageunder the derived key"invoices/<invoice.id>.pdf".fetch_invoice_pdf/1— reads the binary back fromAccrue.Storage. Returns{:error, :not_configured}on theAccrue.Storage.Nulladapter (v1.0 default).
Lazy render rationale
The v1.0 design intentionally never persists PDF bytes because:
- A PDF is a snapshot of (invoice, branding) at one instant. Re-rendering from current DB + current branding preserves visual consistency after a logo/brand change.
- Storage adapters are pluggable and default to
Null; forcing a PDF cache would mean forcing a storage backend. - ChromicPDF renders a typical invoice in < 200ms — cheaper than a DB lookup + byte transfer on most deployments.
See Accrue.Invoices.Render.build_assigns/2 for RenderContext
construction and Accrue.Invoices.Layouts.print_shell/1 for the PDF
HTML shell.
Summary
Functions
Fetches a previously-stored invoice PDF from Accrue.Storage.
Renders an invoice to a PDF binary via the configured Accrue.PDF
adapter.
Renders an invoice and writes the resulting PDF binary to storage
under the derived key "invoices/<invoice.id>.pdf".
Types
@type invoice_or_id() :: Accrue.Billing.Invoice.t() | String.t()
Functions
@spec fetch_invoice_pdf(invoice_or_id()) :: {:ok, binary()} | {:error, term()}
Fetches a previously-stored invoice PDF from Accrue.Storage.
Returns {:error, :not_configured} on the default Accrue.Storage.Null
adapter. Hosts that enable real storage get the bytes back.
@spec render_invoice_pdf( invoice_or_id(), keyword() ) :: {:ok, binary()} | {:error, term()}
Renders an invoice to a PDF binary via the configured Accrue.PDF
adapter.
Accepts either an %Accrue.Billing.Invoice{} struct or an invoice
id string.
Options
:locale— overridescustomer.preferred_localefor money + date formatting (D6-03 precedence: opts > customer > "en"):timezone— overridescustomer.preferred_timezone:archival— whentrue, produces PDF/A (threaded through to ChromicPDF'sprint_to_pdfa/1):size,:paper_width,:paper_height,:margin_top,:margin_bottom,:margin_left,:margin_right— forwarded to the PDF adapter (Pitfall 6: paper size is an adapter option, NOT a CSS rule, because Chromium ignores CSS@pagesize)
Return values
{:ok, binary}— rendered PDF body{:error, %Accrue.Error.PdfDisabled{}}— adapter isAccrue.PDF.Null; caller should fall through to the Stripehosted_invoice_urllink instead of attaching a PDF{:error, :chromic_pdf_not_started}— adapter isAccrue.PDF.ChromicPDFbut the host app has not started ChromicPDF in its supervision tree (Accrue does NOT start ChromicPDF — D-33). Surfaces as a clear, non-retriable error.{:error, term}— any other error raised byRender.build_assigns/2(e.g.,Ecto.NoResultsErrorwhen the id does not exist) is caught and returned as a tagged tuple
@spec store_invoice_pdf( invoice_or_id(), keyword() ) :: {:ok, String.t()} | {:error, term()}
Renders an invoice and writes the resulting PDF binary to storage
under the derived key "invoices/<invoice.id>.pdf".
Returns the canonical storage key on success, the same error tuples
as render_invoice_pdf/2 on render failure, or whatever the storage
adapter returns on write failure.
On the default Accrue.Storage.Null adapter, the key is echoed
back — no bytes are written.