ADR-0002: Receipt PDF design — A4 with top-left total¶
- Status: accepted
- Date: 2026-04-27
- Decider(s): Stefan (constraint), Theo (SA)
Update note (2026-05-02 — see ADR-0004). The "Language selection rule" decision below references
Player.preferred_locale. The locale field has moved toPerson.default_localeper the Person/ClientProfile split. The current language-selection rule isorder.client_profile.person.default_locale ?? Stringer.default_locale— seedocs/architecture/i18n.mdanddocs/architecture/receipts.md. The rule's shape (per-recipient locale wins, falling back to per-stringer default) is unchanged.Receipt numbering is now locked separately in ADR-0008 (in-flight): per-stringer-per-year
YYYY-NNNNminted viaINSERT ... ON CONFLICT DO UPDATEon areceipt_counterstable inside thestrung_at-set transaction.
Context¶
PDF receipts are RBO V2's most physically constrained output. Stefan's stringing-day workflow:
- Strings the racket.
- Prints the receipt on plain A4 paper.
- Folds it twice into a quarter (page midline twice).
- Clips the folded card to the racket handle.
- Customer picks up the racket; what they see at handover is only the top-left quadrant of the page.
The total price must be visible at handover — that's the customer's first signal of "what do I owe?". On a conventional invoice layout, the total goes bottom-right; that's exactly what disappears when you fold an A4 into quarters with the printed side facing in.
Topic 3 confirmed three additional constraints:
- One receipt per order (never batched).
- Finalized at
Strungdate but always re-generatable from order data (FADP basics, not legal-archival). - Both printed and emailed.
- EN + DE template variants (Topic 5).
The design decision is therefore not "which renderer" alone — it's the page layout plus the renderer choice plus the language-template structure, treated as a single locked design.
Options¶
Renderer¶
- WeasyPrint (HTML+CSS → PDF). A4 native, CSS Paged Media support, designers iterate the receipt as a normal HTML page. Pure Python, fits the FastAPI container.
- ReportLab. More code per layout change; harder to iterate the design as Stefan tweaks it.
- Headless Chromium / Puppeteer. Heavyweight container dep for a tiny output volume.
- LaTeX. Overkill, awkward for browser-based iteration.
Layout¶
- Conventional invoice layout (logo top, line items middle, total bottom-right). Fails the fold-into-quarters constraint — total ends up on the inside of the folded card.
- Top-left-quadrant-carries-the-total layout. Total visible at handover. All other quadrants populated with secondary info (string spec, comments, signature) that's available when the receipt is unfolded.
Language variants¶
- One template + i18n string substitution. Cleaner for a SPA; in WeasyPrint, the gain is small and the per-language override of layout micro-tweaks is harder.
- One template per language. Two files, shared base CSS, simpler mental model, easier for a non-programmer to tweak DE strings without touching EN.
Decision¶
- Renderer: WeasyPrint.
- Page: A4, single page per receipt.
- Layout: total price in the top-left quadrant. Specifically: a four-quadrant CSS Grid where
quad-top-leftholds {stringer logo/name, total in CHF at 28pt, racket model, player name}. Other quadrants carry string spec, comments, and signature line. - Templates: one per language, sharing a
base.htmlwith the layout CSS. Files:templates/receipts/receipt_en.html,receipt_de.html. - Language selection rule:
Player.preferred_locale ?? Stringer.default_locale. Per-order override deferred to V3 if asked. - Persistence: none. Receipts are re-rendered on every request; no PDFs cached on disk. Order's denormalized price snapshots (
labor_chf,strings_chf,total_chf) protect historical receipts from drifting if a catalogue price is later updated.
Consequences¶
Good¶
- The fold-into-quarters constraint is structurally enforced by the layout — not a fragile afterthought.
- Re-generation from data is the literal default behavior; no stored-vs-current PDF drift.
- WeasyPrint lets Stefan iterate the design as an HTML preview in a browser, then commit. No round-trip through a heavyweight design tool.
- Adding a language later is one new template file plus a translation pass; no schema change.
Player.preferred_localeis the right granularity — clients of a German-speaking stringer can still receive English receipts.
Costs we accept¶
- Conventional invoice mental model is broken — anyone landing on the receipt for the first time sees the total in an unusual place. We mitigate by making it the largest typographic element on the page.
- No pre-rendered PDFs means a tiny CPU cost on every email + every print. WeasyPrint renders this single-page receipt in well under 1 s; at our volume this never matters.
- Two language templates means a layout tweak takes two file edits — kept manageable by sharing the base CSS, but it's a small ongoing cost as the layout evolves.
- No legal-archival immutability. Acceptable per Topic 1 (hobby, not business; no audit obligation).
Cross-references¶
- Implementation chapter:
docs/architecture/receipts.md. - i18n strategy:
docs/architecture/i18n.md. - Stack pick: ADR-0001.