Accrue.Invoices.Render (accrue v1.0.0)

Copy Markdown View Source

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)

Summary

Functions

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

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

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

Types

opts()

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

Functions

build_assigns(invoice_or_id, opts \\ [])

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(dt, timezone, locale)

@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(amount_minor, currency, locale)

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