Canonical account statement recipe using the Tiered Composition pattern.
Exposes three levels of composability:
document/2— Batteries-included; accepts a statement data map andreturns 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.
The recipe computes the running balance (opening_balance + Σ amount) as an exact Decimal fold and owns per-page chunking so that carried-forward / brought-forward rows land on the correct pages. The engine stays single-pass and behaviorally unchanged.
Data contract
Required keys in data:
:period—%{from: Date.t(), to: Date.t()}(statement period).:account—%{name: String.t()}(account information).:opening_balance—Decimal.t()(balance before the first transaction).:lines—[%{date: Date.t(), description: String.t(), amount: Decimal.t()}](transaction lines; amounts are signed: positive increases the balance, negative decreases it).
Optional keys:
:closing_balance—Decimal.t()(caller assertion; derived and validated viaDecimal.equal?/2when present).:summary—%{total_debits: Decimal.t(), total_credits: Decimal.t(), line_count: non_neg_integer(), closing_balance: Decimal.t()}(caller assertion; derived when absent).
Usage
Zero-to-one (just works)
data = %{
period: %{from: ~D[2026-05-01], to: ~D[2026-05-31]},
account: %{name: "Acme Corp"},
opening_balance: Decimal.new("1000.00"),
lines: [
%{date: ~D[2026-05-02], description: "Invoice #1", amount: Decimal.new("500.00")},
%{date: ~D[2026-05-15], description: "Payment", amount: Decimal.new("-200.00")}
]
}
doc = Rendro.Recipes.Statement.document(data)
{:ok, pdf} = Rendro.render(doc)Escape hatch — inject a custom template
template = Rendro.Recipes.Statement.page_template(name: :my_statement)
sections = Rendro.Recipes.Statement.sections(data)
doc =
Rendro.Document.new()
|> Rendro.Document.add_template(template)
|> Rendro.Document.set_template(:my_statement)
|> 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.Statement.document(data,
formatters: [
amount: fn %Decimal{} = d -> MyApp.Money.format(d) end,
date: fn %Date{} = d -> MyApp.Locale.format_date(d) end
],
labels: %{carried_forward: "Saldo a cuenta nueva"}
)
Summary
Functions
Assembles and returns a fully composed %Rendro.Document{} from a statement
data map.
Returns a %Rendro.PageTemplate{} with three named regions: :header,
:body, and :footer.
Returns a list of %Rendro.Section{} structs mapping statement content to
the :header, :body, and :footer regions.
Functions
@spec document( map(), keyword() ) :: Rendro.Document.t()
Assembles and returns a fully composed %Rendro.Document{} from a statement
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 = %{
...> period: %{from: ~D[2026-05-01], to: ~D[2026-05-31]},
...> account: %{name: "Acme"},
...> opening_balance: Decimal.new("100.00"),
...> lines: []
...> }
iex> doc = Rendro.Recipes.Statement.document(data)
iex> doc.page_template
:statement
@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 (STMT-04 / D-03).
Options
All options are forwarded to %Rendro.PageTemplate{} as keyword overrides.
The name defaults to :statement.
Examples
iex> t = Rendro.Recipes.Statement.page_template()
iex> t.name
:statement
iex> footer = Enum.find(t.regions, & &1.role == :footer)
iex> footer.height > 0
true
@spec sections( map(), keyword() ) :: [Rendro.Section.t()]
Returns a list of %Rendro.Section{} structs mapping statement content to
the :header, :body, and :footer regions.
Validates data via validate_data!/1 before building sections.
Examples
iex> data = %{
...> period: %{from: ~D[2026-05-01], to: ~D[2026-05-31]},
...> account: %{name: "Acme"},
...> opening_balance: Decimal.new("100.00"),
...> lines: []
...> }
iex> [header, body, footer] = Rendro.Recipes.Statement.sections(data)
iex> header.region
:header
iex> footer.region
:footer