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

Invoice render orchestration — builds a single `RenderContext` and
exposes format-helper functions shared by the email + PDF pipelines.

## Contract

`build_assigns/2` is the ONE point of entry. It:

  1. Loads the invoice (+ preloads items + customer) if given an id.
  2. Freezes `Accrue.Config.branding/0` into the struct ONCE (Pitfall 8).
  3. Resolves locale and timezone via the configured precedence ladder:
     `opts > customer column > "en" / "Etc/UTC"`.
  4. Pre-computes every `formatted_*` string via `format_money/3` and
     `format_datetime/3` so templates never call CLDR directly.

## Fail-safe locale + timezone (Pitfall 5)

Both `format_money/3` and `format_datetime/3` wrap their underlying
calls in `try`/`rescue`. On failure they emit a telemetry event and
retry with the safe fallback (`"en"` / `"Etc/UTC"`); they NEVER raise.
This keeps a bad `preferred_locale` or `preferred_timezone` string on
a single customer from breaking the entire email pipeline (one poison
row stays isolated).

Events emitted:

  * `[:accrue, :email, :locale_fallback]` — metadata
    `%{requested: locale, currency: currency}` (no PII)
  * `[:accrue, :email, :timezone_fallback]` — metadata
    `%{requested: timezone}`
  * `[:accrue, :email, :format_money_failed]` — metadata
    `%{requested: locale, currency: currency}` (second-attempt failure)

# `opts`

```elixir
@type opts() :: [
  locale: String.t() | nil,
  timezone: String.t() | nil,
  customer: term() | nil,
  now: DateTime.t() | nil
]
```

# `build_assigns`

```elixir
@spec build_assigns(Accrue.Billing.Invoice.t() | String.t(), opts()) ::
  Accrue.Invoices.RenderContext.t()
```

Builds a `RenderContext` from an `Invoice` struct or an invoice id.

`opts`:
  * `:locale` — overrides customer.preferred_locale
  * `:timezone` — overrides customer.preferred_timezone
  * `:customer` — pre-loaded customer, skipping the Repo fetch
  * `:now` — pin the "issued at" timestamp (defaults to `DateTime.utc_now/0`)

# `format_datetime`

```elixir
@spec format_datetime(DateTime.t() | nil, String.t(), String.t()) :: String.t() | nil
```

Formats a `DateTime` into a human-readable string in `timezone`.

On timezone shift failure (unknown TZ or missing tzdata), emits
`[:accrue, :email, :timezone_fallback]` and retries with `"Etc/UTC"`.
Locale is accepted for forward-compatibility but v1.0 uses
`Calendar.strftime/2` (en-only) — hosts override via a custom
formatter in a future release.

# `format_money`

```elixir
@spec format_money(integer(), atom(), String.t()) :: String.t()
```

Formats an integer minor-unit amount into a human-readable currency
string using the given locale.

NEVER raises — on unknown locale/currency it emits a telemetry
fallback event and retries with `"en"`; on second failure it emits a
hard-failure event and returns a raw fallback like `"1000 usd"`.

---

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