Skip to main content
Logo

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:

DocumentWhen it printsTypical printer
Sales receiptPayment is completedFront-counter
Kitchen ticketOrder is created (for kitchen items)Kitchen
Bar ticketOrder is created (for bar items)Bar
Customer billServer requests "Bill out" before paymentFront-counter
Official receiptFiscal copy required (e.g. BIR, VAT)Front-counter
Refund receiptRefund or void is processedFront-counter
Queue ticketCustomer joins the queueFront-counter / queue
Product labelItem label / barcode is printedLabel printer
Delivery slipOrder is dispatchedFront-counter / kitchen
Test pagePrinter setup / diagnosticAny

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: the item name and its line total sit on the first row (total right-aligned); when the quantity is more than one, a subtle qty @ unit breakdown prints beneath, with modifiers indented under that.
  • Dividers: = for major section breaks (around the total), - for minor ones. Every divider spans the full paper width and does the visual separating — the layout avoids stacking blank lines to save paper.
  • Headers: store name printed bold and double-height for instant recognition. Optional lines (address, phone, tax ID) print only when your store has them — no blank rows.
  • Item count: receipts close the item list with an Items: N summary.

These tokens live in packages/print-engine/src/design/tokens.ts. Changing them ripples through every template in a single place.

Money & dates

These two details do the most to make a receipt look professional.

Money prints with thousands separators, left-labelled and right-aligned (Subtotal 1,234.56). Amounts are bare numbers and the currency is shown once in the receipt header (Currency PHP) — cleaner than repeating a prefix on every line, and it sidesteps a real hardware limitation: many low-cost Bluetooth printers cannot render the peso glyph and would print ? in its place. (The engine can also print a per-amount symbol or ISO-code prefix where a printer is known to support it.)

Dates print as Apr 15, 2026 2:32 PM — never a raw timestamp. When your store's timezone is known, times are shown in that zone.

The audit line

Back-office copies (merchant and accounting copies, like the end-of-day session report) carry a tiny TPL:<name> FP:<hash> line for support traceability and tamper-checking. Customer-facing receipts, kitchen tickets, and delivery slips do not print it — it would only be noise on the customer's copy.

"Not an official receipt" notice

Until your store has a BIR Permit to Print Official Receipts, the sales receipt prints a THIS IS NOT AN OFFICIAL RECEIPT line near the bottom. Once your permit is approved, this notice is removed and your fiscal copies print as the Official Receipt document (with your TIN, OR number, and legal footer).

The sales receipt also shows the cashier / assigned staff, the order
type
(Dine In, Take Out, Delivery…), the payment tendered, and any
change — each line appears only when that information is available.

Kitchen & bar tickets

Kitchen and bar tickets are built for the line — large, scannable, and price-free. Every ticket follows the same top-to-bottom flow:

  1. Store name and outlet, then a bold KITCHEN ORDER (or BAR ORDER) band.
  2. A large order number.
  3. The order typeDine In, Take Out, Delivery — printed prominently so the kitchen routes it correctly.
  4. The details it has: customer, table, guests, the server who rang it in, and the time placed. Each line appears only when that information exists.
  5. An ORDER NOTE block for any order-wide special instructions ("Customer allergic to peanuts. Please rush.").
  6. The items — each numbered, double-height, with modifiers, prep notes (*), and allergen tags indented underneath.
  7. A closing TOTAL ITEMS count (or TOTAL DRINKS on a bar ticket).

Prices never print on a kitchen or bar ticket — they're not for the customer.

Every document Fuze Store prints — receipt, kitchen ticket, refund, delivery
slip, session report — opens the same way: your store name, then a clearly
framed title band. One consistent look across the whole roll.

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:

  1. Pick a key name (label.subtotal, refund.reason, etc.).
  2. Add a translation in your locale dictionary on the client.
  3. Pass it through options.translations when calling renderPrintJob.

The templates are fully localization-ready. Today the apps print the engine's built-in English labels; wiring each app's active language into options.translations so receipts print in your account language is a planned follow-up. (Your store's own data — item names, customer names, notes — already prints exactly as you entered it, in any language.)

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   │
  └─────────────────────────────┘
  1. The POS builds a PrintJobInput from the order, store, and printer profile.
  2. The print engine renders an intermediate representation (IR) — a structured list of operations with alignment, bold and double-size flags.
  3. The mobile app converts the IR to Earl DSL (<C><BOLD>...</BOLD></C>) for transport over Bluetooth and the LAN bridge.
  4. For LAN printers needing rich features (QR codes, barcodes), the bridge accepts raw ESC/POS bytes via POST /print/escpos, bypassing the DSL.
  5. 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:

  1. Dropping a JSON file in src/templates/.
  2. Registering it in src/template/registry.ts.
  3. Adding a snapshot case in test/snapshots.spec.ts.
  4. 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.