Receipt & Ticket Design
Every piece of paper Fuze Store prints — sales receipts, kitchen tickets, bar slips, customer bills, official receipts, refund receipts, queue stubs, product labels, and delivery slips — comes out of a single, shared print engine. The same engine renders the preview you see in the app and the bytes that go to your physical printer.
This page explains the design system behind those documents so you can predict what will print, configure your store correctly, and (if you’re a developer) extend it safely.
You don’t have to read this to get printing working. Connect a printer, hit
print, and Fuze Store does the right thing. This page is for store owners who
want fine control and developers building on top of the platform.
What gets printed
Fuze Store ships nine document types out of the box, plus a diagnostic test page:
| Document | When it prints | Typical printer |
|---|---|---|
| Sales receipt | Payment is completed | Front-counter |
| Kitchen ticket | Order is created (for kitchen items) | Kitchen |
| Bar ticket | Order is created (for bar items) | Bar |
| Customer bill | Server requests "Bill out" before payment | Front-counter |
| Official receipt | Fiscal copy required (e.g. BIR, VAT) | Front-counter |
| Refund receipt | Refund or void is processed | Front-counter |
| Queue ticket | Customer joins the queue | Front-counter / queue |
| Product label | Item label / barcode is printed | Label printer |
| Delivery slip | Order is dispatched | Front-counter / kitchen |
| Test page | Printer setup / diagnostic | Any |
Each document is defined by a JSON template in the print engine. Templates describe blocks (text, key/value rows, line items, dividers, QR codes, etc.) — they never describe pixels. The engine takes care of column math, wrapping, alignment, and ESC/POS formatting for both 58mm and 80mm paper.
Design tokens
To keep every document consistent, the print engine has a single source of truth for layout numbers:
- Columns: 32 columns for 58mm paper, 48 columns for 80mm paper.
- Line item layout: a 3-column
qty | name | pricegrid with the price column right-aligned and the name column wrapping when long. - Dividers:
=for major section breaks,-for minor ones. Every divider spans the full paper width. - Section gaps: a single blank line between sections — never more, never less.
- Headers: store name printed double-width and double-height for instant recognition.
- Fingerprint footer: every print includes a tiny
TPL:<name> FP:<hash>line for support traceability.
These tokens live in packages/print-engine/src/design/tokens.ts. Changing them ripples through every template in a single place.
Internationalization (i18n)
Templates never embed English strings directly. Instead they reference translation keys:
{
"type": "keyValue",
"left": "{{i18n:label.subtotal}}",
"right": "{{fmt.transaction.subtotal}}"
}
When the engine renders a job it merges the built-in English defaults with any overrides you pass through options.translations. Missing keys render as the key itself (e.g. label.subtotal) so drift is visible on the printed paper, not silently swallowed.
To localize:
- Pick a key name (
label.subtotal,refund.reason, etc.). - Add a translation in your locale dictionary on the client.
- Pass it through
options.translationswhen callingrenderPrintJob.
The mobile app already wires this up — receipts honor your account language without any code changes from you.
Fiscal & legal footers
Compliance text (BIR footer in PH, VAT registration line in EU, etc.) is never hard-coded into templates. Instead, your store carries a small fiscal config:
type StoreFiscalConfig = {
tin?: string; // Tax ID printed on official receipts
orPrefix?: string; // e.g. "OR No.", "Invoice #"
legalFooters?: string[]; // Free-form footer lines
jurisdictionId?: string; // e.g. "PH-BIR-V1"
};
The mobile bridge reads this from store.preferences.fiscal and forwards it to the print engine. Templates that need the TIN, OR number, or footer block pick it up automatically. Stores without a fiscal config simply omit those lines.
This means:
- Switching tax regions is a config change, not a code change.
- Multiple outlets in different jurisdictions can each carry their own legal footer.
- No template duplication for "PH official receipt", "MX official receipt", etc.
How a print job flows
┌─────────────────────────┐
│ POS (mobile / web) │
│ buildPrintJobInput() │
└────────────┬────────────┘
│ PrintJobInput (JSON)
▼
┌─────────────────────────┐
│ @fuze-store/print-engine
│ renderPrintJob() │ → IR (intermediate representation)
└────────────┬────────────┘
│
┌────────┴────────┐
│ │
▼ ▼
┌─────────┐ ┌──────────────┐
│ Earl │ │ Raw ESC/POS │
│ DSL │ │ bytes │
│ (BLE + │ │ (LAN: QR / │
│ LAN) │ │ barcodes) │
└────┬────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────────────────────┐
│ ESC/POS thermal printer │
└─────────────────────────────┘
- The POS builds a
PrintJobInputfrom the order, store, and printer profile. - The print engine renders an intermediate representation (IR) — a structured list of operations with alignment, bold and double-size flags.
- The mobile app converts the IR to Earl DSL (
<C><BOLD>...</BOLD></C>) for transport over Bluetooth and the LAN bridge. - For LAN printers needing rich features (QR codes, barcodes), the bridge accepts raw ESC/POS bytes via
POST /print/escpos, bypassing the DSL. - Either path produces the same printed output because both originate from the same IR.
Snapshot guarantees
Every document has a snapshot test at both 58mm and 80mm. CI fails if a template change accidentally moves a column, swaps an alignment, or drops a translation key. So you can iterate on templates with confidence — the diff is the design.
Test page
If a print looks wrong, run a test print from Settings → Printers → <printer> → Test print. The test page exercises:
- Centering, left and right alignment.
- Bold and double-width / double-height styles.
- A character ruler so you can spot column drift.
- Date/time and terminal name to confirm the device is online.
A successful test page means the printer, paper width, and bridge are all configured correctly. If your real receipts still look off, the issue is data-side (e.g. missing TIN, wrong locale) — not printer-side.
For developers
The engine lives at packages/print-engine in the monorepo:
src/templates/*.json— the document templates.src/design/tokens.ts— column counts, item layout, design constants.src/design/translations.ts— default English translations.src/emitter/earl.ts— IR → Earl DSL converter.src/pipeline.ts— the renderer entry point (renderPrintJob).test/snapshots.spec.ts— the locking snapshot suite.
Add a new document by:
- Dropping a JSON file in
src/templates/. - Registering it in
src/template/registry.ts. - Adding a snapshot case in
test/snapshots.spec.ts. - Wiring a builder in
apps/mobile/src/services/printer/printEngineBridge.ts.
That’s it. The same template will render identically on Bluetooth, LAN, and the in-app preview.