merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 85b040a75c6185c4c5f0f0bd16f3aba4901e64fc
parent 46fb385b853733fa54234ae5cdf990a36a18c879
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri, 20 Feb 2026 23:44:13 +0100

add template for PDF for GET /private/orders

Diffstat:
Mcontrib/typst/Makefile.am | 1+
Acontrib/typst/orders.typ | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 178 insertions(+), 0 deletions(-)

diff --git a/contrib/typst/Makefile.am b/contrib/typst/Makefile.am @@ -2,6 +2,7 @@ SUBDIRS = . formdatadir = $(datadir)/taler-merchant/typst-forms/ dist_formdata_DATA = \ + orders.typ \ transactions.typ EXTRA_DIST = \ diff --git a/contrib/typst/orders.typ b/contrib/typst/orders.typ @@ -0,0 +1,177 @@ +// Helper function to format a Taler timestamp object {t_s: ...} +#let format_timestamp(ts) = { + if type(ts) == dictionary and "t_s" in ts { + let t_s = ts.t_s + if t_s == "never" { + "never" + } else { + let seconds = int(t_s) + let days_since_epoch = calc.quo(seconds, 86400) + let remaining_seconds = calc.rem(seconds, 86400) + let hours = calc.quo(remaining_seconds, 3600) + let minutes = calc.quo(calc.rem(remaining_seconds, 3600), 60) + let secs = calc.rem(remaining_seconds, 60) + + let is_leap(y) = { + calc.rem(y, 4) == 0 and (calc.rem(y, 100) != 0 or calc.rem(y, 400) == 0) + } + + let year = 1970 + let days_left = days_since_epoch + let done = false + while not done { + let days_in_year = if is_leap(year) { 366 } else { 365 } + if days_left >= days_in_year { + days_left = days_left - days_in_year + year = year + 1 + } else { + done = true + } + } + + let days_in_months = if is_leap(year) { + (31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } else { + (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + } + + let month = 1 + for days_in_month in days_in_months { + if days_left >= days_in_month { + days_left = days_left - days_in_month + month = month + 1 + } else { + break + } + } + let day = days_left + 1 + + let m_str = if month < 10 { "0" + str(month) } else { str(month) } + let d_str = if day < 10 { "0" + str(day) } else { str(day) } + let h_str = if hours < 10 { "0" + str(hours) } else { str(hours) } + let min_str = if minutes < 10 { "0" + str(minutes) } else { str(minutes) } + let s_str = if secs < 10 { "0" + str(secs) } else { str(secs) } + + str(year) + "-" + m_str + "-" + d_str + " " + h_str + ":" + min_str + ":" + s_str + " UTC" + } + } else { + str(ts) + } +} + +// Format a Taler amount. +// Taler serialises amounts as a plain string "CURRENCY:VALUE.FRACTION", +// e.g. "EUR:5.50". If the value is null / none we render a dash. +#let format_amount(a) = { + if a == none { + "-" + } else { + // a is already a human-readable string like "EUR:5.50" + str(a) + } +} + +#let form(data) = { + set page( + paper: "a4", + margin: (left: 2cm, right: 2cm, top: 2cm, bottom: 2.5cm), + footer: context [ + #grid( + columns: (1fr, 1fr), + align: (left, right), + text(size: 8pt)[], + text(size: 8pt)[ + Page #here().page() of #counter(page).final().first() + ] + ) + ] + ) + + heading(level: 1)[GNU Taler Merchant Orders: #data.business_name] + + v(0.5cm) + + table( + columns: (auto, auto, auto, auto, auto, auto, auto), + align: (left, left, left, right, right, center, center), + // Header row + table.header( + [*Order ID*], + // [*Row*], + [*Summary*], + [*Timestamp*], + [*Amount*], + [*Refund*], + // [*Pending refund*], + [*Refundable*], + [*Paid*], + ), + // Data rows + ..data.orders.map(o => ( + o.order_id, + // str(o.row_id), + o.summary, + format_timestamp(o.timestamp), + format_amount(o.amount), + format_amount(o.at("refund_amount", default: none)), + // format_amount(o.at("pending_refund_amount", default: none)), + if o.refundable { "✓" } else { "✗" }, + if o.paid { "✓" } else { "✗" }, + )).flatten(), + // Bold totals row spanning the label across the first 4 columns, + // then the three amount totals, then two empty trailing cells. + table.footer( + table.cell(colspan: 3)[*Totals*], + table.cell(align: right)[ + *#format_amount(data.total_amount)* + ], + table.cell(align: right)[ + *#format_amount(data.total_refund_amount)* + ], + // table.cell(align: right)[ + // *#format_amount(data.total_pending_refund_amount)* + // ], + [], + [], + ), + ) +} + +// Example usage: +#form(( + business_name: "example.com", + orders: ( + ( + order_id: "2025.001", + row_id: 1, + summary: "Some purchase", + timestamp: (t_s: 1764967786), + amount: "EUR:10.00", + paid: true, + refundable: false, + ), + ( + order_id: "2025.002", + row_id: 2, + summary: "Refunded order", + timestamp: (t_s: 1764970000), + amount: "EUR:5.50", + refund_amount: "EUR:2.00", + pending_refund_amount: "EUR:1.00", + paid: true, + refundable: true, + ), + ( + order_id: "2025.003", + row_id: 3, + summary: "Another order", + timestamp: (t_s: 1764975000), + amount: "EUR:3.25", + paid: false, + refundable: false, + ), + ), + total_amount: "EUR:18.75", + total_refund_amount: "EUR:2.00", + total_pending_refund_amount: "EUR:1.00", +))