PRD: Receipt templates
| Module | Commerce (CORE-03) | PRD ID | PRD-RCP-001 |
| Status | Shipped | Owner | Commerce squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/commerce · @nx/core · apps/sale-renderer | URD | RCP |
TL;DR
Lets a merchant design exactly how their printed receipt looks - per merchant or per sale channel, and per language. A template is a named, ordered layout assembled from a fixed set of blocks (text, line, image, a line-item table, barcode, QR code, and a grid for side-by-side rows), each binding to live order data and carrying its own styling. The layout is validated on save against the block vocabulary, so a malformed receipt never reaches the printer. Exactly one template is the default per language for each merchant or channel - promoting a new default automatically retires the old one - and at print time the chosen template renders to a monochrome raster sized for 58mm or 80mm thermal paper.
1. Context & Problem
Every sale ends in a printed receipt, but merchants do not all want the same one. A coffee shop wants its logo and a QR code for the loyalty program; a retail counter wants a barcode and a compact line-item table; a multilingual venue wants a Vietnamese receipt at one channel and an English one at another. Hard-coding a single receipt layout forces all of them onto the same slip.
Without a template surface, a merchant cannot change the header, reorder sections, add a barcode, or print in a second language without an engineering change. There is also no safe place to store these layouts: a free-form blob would let a malformed receipt - a column with no field, a grid that never terminates - slip through and jam the printer at the worst moment, mid-checkout.
This increment gives Commerce a first-class receipt-template entity: named, scoped to a merchant or a sale channel, locale-aware, built from a fixed and validated block vocabulary, with one default per language, and rendered to the exact raster a thermal printer expects.
2. Goals & Non-Goals
Goals
- A named receipt template scoped to either a merchant or a sale channel, in a chosen language.
- Layout assembled from a fixed block vocabulary - text, line, image, line-item table, barcode, QR code, and a grid for side-by-side rows.
- Validate the layout on every save - a template that does not conform to the block vocabulary is refused before it is stored.
- One default per (principal, locale) - promoting a default automatically unsets the previous one for that scope and language.
- Blocks bind to live order data (fields, item rows) resolved at print time, with per-field number / currency / date formatting.
- Render a template to a monochrome raster sized for 58mm or 80mm thermal paper.
- Full lifecycle - create, edit, list/filter, status (Activated / Deactivated / Archived), soft-delete - scoped per merchant.
Non-Goals
- A WYSIWYG drag-and-drop editor UI - the template builder is a client surface; this PRD specifies the template contract and rendering, not the editing canvas.
- E-invoice / legal tax-invoice layout and issuance - the fiscal invoice is governed by Tax & Invoice, not by these print templates.
- Printer transport / driver selection (USB, Bluetooth, network) - the template produces a raster; delivering it to a device is a renderer concern.
- Email / PDF receipts - this increment targets thermal print only.
- Cross-merchant template sharing or a global template marketplace.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Layout safety | A malformed layout (missing field, bad block type) is refused on save - zero malformed templates reach a printer |
| Default integrity | Each (merchant-or-channel, locale) has at most one default template at all times |
| Scope coverage | A template can be attached to a merchant or to a specific sale channel, in a chosen language |
| Render fidelity | A template renders to a raster of the correct width for its paper size (58mm or 80mm) |
| Data binding | Order fields and line items appear on the printed slip via their declared field bindings and formats |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Design the receipt - branding, sections, barcode/QR - and set the default per language |
| Channel operator | Give a specific sale channel its own receipt layout |
| Cashier | Print a correct, readable receipt at checkout without configuring anything |
Core scenario: an owner designs an 80mm receipt for their merchant - a logo image, a centered shop name, a dashed separator, a line-item table (name · qty · price), a totals grid, and a QR code bound to the order reference. They mark it the default for Vietnamese. The layout is validated and stored. At checkout the cashier completes payment; the system picks the merchant's default Vietnamese template, binds the order's fields and items into it, and renders a 576px-wide monochrome raster the thermal printer prints. Later the owner adds an English template for their take-away channel and marks it default for English - the previous English default for that channel is retired automatically.
5. User Stories
- As an owner, I design my receipt from a fixed set of blocks (text, image, separators, an item table, barcode, QR), so the slip reflects my brand without an engineering change.
- As an owner, I set one default receipt per language, so checkout always picks a sensible layout.
- As a channel operator, I give one sale channel its own receipt, so a delivery slip can differ from the dine-in slip.
- As an owner, I bind the item table and totals to live order data, so each receipt prints the real order.
- As an owner, I want a broken layout rejected when I save it, so I never discover the problem at the printer mid-sale.
- As a cashier, I print a correct receipt with no setup, so checkout is one tap.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | A named receipt template is scoped to either a merchant or a sale channel, in a chosen locale | URD-RCP-001 · URD-RCP-004 |
| FR-2 | Each template declares a paper width (58mm or 80mm) and a base font size | URD-RCP-002 |
| FR-3 | Template name is unique per (principal, locale) - the same name is reusable across languages and is soft-delete aware | URD-RCP-003 |
| FR-4 | Layout is an ordered list of blocks from a fixed vocabulary: text, line, image, line-item table, barcode, QR code, grid | URD-RCP-006 |
| FR-5 | The layout is validated against the block vocabulary on create and update; a non-conforming layout is refused | URD-RCP-007 |
| FR-6 | Text, line-item, barcode, and QR blocks bind to dynamic order fields resolved at print time | URD-RCP-008 |
| FR-7 | The line-item table renders configurable columns (field · header · width · align · format) with an optional header row | URD-RCP-009 |
| FR-8 | A grid block lays children into proportional columns with alignment / justification for side-by-side rows (up to 5) | URD-RCP-010 |
| FR-9 | Blocks carry presentation styling - alignment, weight, transform, decoration, borders, colors, padding | URD-RCP-011 |
| FR-10 | Numeric, currency, and date values format per a declared format on the block or column | URD-RCP-012 |
| FR-11 | Exactly one template is the default per (principal, locale); promoting a default unsets the previous one atomically | URD-RCP-005 |
| FR-12 | A template renders to a monochrome raster sized for its paper width (58mm → 384px, 80mm → 576px) | URD-RCP-013 |
| FR-13 | Templates carry a lifecycle status (Activated / Deactivated / Archived) and are soft-deleted | URD-RCP-014 |
| FR-14 | All operations are scoped per merchant (x-merchant-id) and gated by receipt-template permissions; templates are listable/filterable by principal, locale, status, and default flag | URD-RCP-015 · URD-RCP-016 |
Full requirement text and acceptance criteria live in the Commerce URD - RCP. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Layout integrity | The layout is strictly parsed against the fixed block vocabulary on save; a non-conforming layout is rejected with a clear reason, never stored |
| Default consistency | Setting a default and retiring the previous one for the same (principal, locale) happen in one atomic transaction |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id); gated by receipt-template create / read / update / delete permissions under Commerce |
| Scope polymorphism | A template attaches to a merchant or a sale channel via a single polymorphic principal reference |
| i18n | Templates are per-locale; a merchant or channel keeps one template set per language and one default per language |
| Render determinism | A given template + order data renders to a deterministic monochrome raster of the correct width for the paper size |
| Soft-delete | Templates are never physically removed; name and default uniqueness are evaluated only over live rows |
8. UX & Flows
Authoring & default resolution
Print at checkout
The authoring surface (in apps/sale-renderer printer settings) lets the owner pick the scope (merchant or channel) and language, choose the paper width, assemble the ordered blocks, bind each block to its data field, preview the rendered slip, and mark the template default for that language.
9. Data & Domain
| Entity | Role |
|---|---|
ReceiptTemplate | A named, locale-aware layout scoped to a merchant or sale channel; carries paper width, base font size, default flag, status, and the block layout |
| Block vocabulary | The fixed set of layout elements - text, line, image, line-item table, barcode, QR code, and a recursive grid - each with its own styling and (where relevant) data binding |
| Principal reference | The single polymorphic link that scopes a template to a merchant or a sale channel |
Conceptual only - the full block vocabulary, field bindings, and invariants live in the commerce domain model. The block layout is stored as a structured document and validated on every write.
10. Dependencies & Assumptions
Depends on
- Merchant & sale channel (MER, SC) - a template's principal is one of these; the scope must exist first.
- Permissions (Permissions) - receipt-template create / read / update / delete are gated under Commerce.
- Sale / order data (Orders) - blocks bind to order fields and line items resolved at print time.
@nx/core- the template entity, the block vocabulary, and the rendering utility.
Assumptions
- The merchant or sale channel a template targets already exists.
- The point-of-sale renderer prints a monochrome raster to a 58mm or 80mm thermal printer.
- A merchant chooses a language per template; an order carries enough fields and line items to populate the bound blocks.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| A malformed layout jams the printer mid-sale | Layout is strictly validated on every save against the block vocabulary; a non-conforming template is refused before it is stored |
| Two defaults for the same scope and language | Promoting a default atomically retires the previous default for that (principal, locale) |
| Wrong-width raster on the wrong printer | The template declares its paper width; rendering sizes the raster to it (58mm → 384px, 80mm → 576px) |
| A removed template's name blocks re-use | Name and default uniqueness are soft-delete aware - evaluated only over live rows |
| No visual editor specified here | Deliberate - the builder is a client surface; this PRD fixes the template contract and rendering, not the canvas |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - RCP in the URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None - new entity; merchants without a template fall back to the built-in default slip |
| Launch criteria | Templates create / edit scoped to a merchant or channel per language; a malformed layout is refused on save; exactly one default per (principal, locale); a template renders to a raster of the correct width and prints at checkout with order data bound |
| Monitoring | Save-rejection rate by reason (invalid layout), default-conflict occurrences, render errors per merchant |
13. FAQ
Can each sale channel have its own receipt? Yes - a template is scoped to either a merchant or a specific sale channel, so a delivery channel can print a different slip from dine-in.
How does language work? Templates are per-locale. A merchant or channel keeps a template per language and one default per language; checkout picks the default for the order's language.
What happens if my layout is broken? It is refused when you save it - the layout is validated against the fixed block vocabulary, so a missing column field or an unknown block type is caught before it can ever reach a printer.
Can I have two default receipts? Not for the same scope and language - marking a template default automatically retires the previous default for that merchant-or-channel and locale.
What can I put on a receipt? Text, separators (lines), an image (e.g. logo), a line-item table with configurable columns, a barcode, a QR code, and a grid for side-by-side rows - each with its own alignment, styling, and data binding.
Does this print legal tax invoices? No - the fiscal e-invoice is governed by Tax & Invoice. These templates are for the printed sales receipt.
References
- URD: Commerce - RCP
- Sibling PRDs: Organization & merchant · Sale channels · Categories
- Module: Commerce - overview + capabilities
- Developer: @nx/commerce · @nx/core