# `Accrue.Invoices`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.0.0/lib/accrue/invoices.ex#L1)

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 is `Accrue.PDF.Null`, and `{:error, :chromic_pdf_not_started}`
    when the configured adapter is `Accrue.PDF.ChromicPDF` but the
    ChromicPDF GenServer is not running in the host supervision tree
    (D6-04 safety net — Pitfall 4).
  * `store_invoice_pdf/2` — renders via `render_invoice_pdf/2` and
    writes the binary to `Accrue.Storage` under the derived key
    `"invoices/<invoice.id>.pdf"`.
  * `fetch_invoice_pdf/1` — reads the binary back from
    `Accrue.Storage`. Returns `{:error, :not_configured}` on the
    `Accrue.Storage.Null` adapter (v1.0 default).

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

# `invoice_or_id`

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

# `fetch_invoice_pdf`

```elixir
@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`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
