Rendro.Recipes.Receipt (Rendro v1.0.0) (adapter)

Copy Markdown View Source

Payment receipt and tabular report recipe using the Tiered Composition pattern.

A single-page receipt and a multi-page tabular "report" are the same recipe — multi-page is just a receipt whose line items overflow one page. Column headers repeat on every page via per-page table blocks; "Page X of Y" appears in the footer via Rendro.page_number/1.

Exposes three levels of composability:

  • document/2 — Batteries-included; accepts a receipt data map and
                    returns a fully assembled `%Rendro.Document{}` ready
                    for `Rendro.render/1`. No template authoring required.
  • page_template/1 — Layout only; returns the %Rendro.PageTemplate{}.
  • sections/2 — Content only; returns a list of %Rendro.Section{}
                    structs mapped to named regions.

Data contract

Required keys in data:

  • :titleString.t() (e.g. "Payment Receipt").
  • :dateDate.t() (issue date).
  • :customer%{name: String.t()} (customer information).
  • :lines[%{description: String.t(), amount: Decimal.t()}] (line items; amounts must be Decimal, not Float).

Optional keys:

  • :totals%{subtotal: Decimal.t(), total: Decimal.t()} (caller assertions; subtotal is validated against the sum of line amounts via Decimal.equal?/2 when present).

Usage

Zero-to-one (just works)

data = %{
  title: "Payment Receipt",
  date: ~D[2026-05-29],
  customer: %{name: "Acme Corp"},
  lines: [
    %{description: "Widget A", amount: Decimal.new("29.99")},
    %{description: "Widget B", amount: Decimal.new("49.99")}
  ],
  totals: %{subtotal: Decimal.new("79.98"), total: Decimal.new("79.98")}
}
doc = Rendro.Recipes.Receipt.document(data)
{:ok, pdf} = Rendro.render(doc)

Escape hatch — inject a custom template

template = Rendro.Recipes.Receipt.page_template(name: :my_receipt)
sections = Rendro.Recipes.Receipt.sections(data)
doc =
  Rendro.Document.new()
  |> Rendro.Document.add_template(template)
  |> Rendro.Document.set_template(:my_receipt)
  |> then(fn d -> Enum.reduce(sections, d, &Rendro.Document.add_section(&2, &1)) end)

Formatting

Default formatting is provided by Rendro.Format (pure, locale-free, deterministic): money as $1,234.50 (parentheses for negatives) and dates as YYYY-MM-DD.

Override defaults via opts:

Rendro.Recipes.Receipt.document(data,
  formatters: [
    amount: fn %Decimal{} = d -> MyApp.Money.format(d) end,
    date:   fn %Date{} = d   -> MyApp.Locale.format_date(d) end
  ]
)

Summary

Functions

Assembles and returns a fully composed %Rendro.Document{} from a receipt data map.

Returns a %Rendro.PageTemplate{} with three named regions: :header, :body, and :footer.

Returns a list of %Rendro.Section{} structs mapping receipt content to the :header, :body, and :footer regions.

Functions

document(data, opts \\ [])

@spec document(
  map(),
  keyword()
) :: Rendro.Document.t()

Assembles and returns a fully composed %Rendro.Document{} from a receipt data map.

Validates data via validate_data!/1, then builds the page template and sections, reducing them through the Document builder API.

Examples

iex> data = %{
...>   title: "Payment Receipt",
...>   date: ~D[2026-05-29],
...>   customer: %{name: "Acme Corp"},
...>   lines: []
...> }
iex> doc = Rendro.Recipes.Receipt.document(data)
iex> doc.page_template
:receipt

page_template(opts \\ [])

@spec page_template(keyword()) :: Rendro.PageTemplate.t()

Returns a %Rendro.PageTemplate{} with three named regions: :header, :body, and :footer.

The footer region has a non-zero height so body_capacity reserves space for the "Page X of Y" page-number text (PAGE-03).

Options

All options are forwarded to %Rendro.PageTemplate{} as keyword overrides. The name defaults to :receipt.

Examples

iex> t = Rendro.Recipes.Receipt.page_template()
iex> t.name
:receipt
iex> footer = Enum.find(t.regions, & &1.role == :footer)
iex> footer.height > 0
true

sections(data, opts \\ [])

@spec sections(
  map(),
  keyword()
) :: [Rendro.Section.t()]

Returns a list of %Rendro.Section{} structs mapping receipt content to the :header, :body, and :footer regions.

Validates data via validate_data!/1 before building sections.

Examples

iex> data = %{
...>   title: "Payment Receipt",
...>   date: ~D[2026-05-29],
...>   customer: %{name: "Acme Corp"},
...>   lines: []
...> }
iex> [header, body, footer] = Rendro.Recipes.Receipt.sections(data)
iex> header.region
:header
iex> footer.region
:footer