Format-neutral hydrated invoice payload (D6-04).
Built once per render by Accrue.Invoices.Render.build_assigns/2 and
passed through BOTH the email body (via <mj-raw> + HtmlBridge) and
the PDF shell (via Accrue.Invoices.Layouts.print_shell/1). This
guarantees every format sees byte-identical money strings and the
same frozen branding snapshot.
Branding snapshot freeze (Pitfall 8)
The :branding field holds a keyword list captured at build time from
Accrue.Config.branding/0. It MUST NOT be re-read downstream — if a
component needs branding it reads ctx.branding. This prevents config
drift between the email header, the PDF header, and the footer within
a single render.
Pre-formatted fields
Money + date fields are pre-formatted into formatted_* strings at
build time (CLDR calls live off the hot template path). Components
read ctx.formatted_total directly instead of calling format_money/3
from inside ~H.
Summary
Types
@type t() :: %Accrue.Invoices.RenderContext{ branding: keyword(), currency: atom(), customer: term(), discount_minor: integer() | nil, formatted_discount: String.t() | nil, formatted_issued_at: String.t() | nil, formatted_subtotal: String.t() | nil, formatted_tax: String.t() | nil, formatted_total: String.t() | nil, hosted_invoice_url: String.t() | nil, invoice: term(), line_items: [term()] | nil, locale: String.t(), now: DateTime.t() | nil, receipt_url: String.t() | nil, subtotal_minor: integer() | nil, tax_minor: integer() | nil, timezone: String.t(), total_minor: integer() | nil }