Accrue.Invoices (accrue v1.0.0)

Copy Markdown View Source

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

Lazy render rationale

The v1.0 design intentionally never persists PDF bytes because:

  1. 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.
  2. Storage adapters are pluggable and default to Null; forcing a PDF cache would mean forcing a storage backend.
  3. 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

invoice_or_id()

@type invoice_or_id() :: Accrue.Billing.Invoice.t() | String.t()

Functions

fetch_invoice_pdf(invoice_or_id)

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

render_invoice_pdf(invoice_or_id, opts \\ [])

@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 — overrides customer.preferred_locale for money + date formatting (D6-03 precedence: opts > customer > "en")
  • :timezone — overrides customer.preferred_timezone
  • :archival — when true, produces PDF/A (threaded through to ChromicPDF's print_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 @page size)

Return values

  • {:ok, binary} — rendered PDF body
  • {:error, %Accrue.Error.PdfDisabled{}} — adapter is Accrue.PDF.Null; caller should fall through to the Stripe hosted_invoice_url link instead of attaching a PDF
  • {:error, :chromic_pdf_not_started} — adapter is Accrue.PDF.ChromicPDF but 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 by Render.build_assigns/2 (e.g., Ecto.NoResultsError when the id does not exist) is caught and returned as a tagged tuple

store_invoice_pdf(invoice_or_id, opts \\ [])

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