# Receipt & Ticket Design
> Source: https://development.fuze-store.com/docs/app/printer/receipt-design

# 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 |
| **Order sticker** | Order moves into preparation | Label printer |
| **Delivery slip** | Order is dispatched | Front-counter / kitchen |
| **Test page** | Printer setup / diagnostic | Any |

The **order sticker** prints one small label per item-unit as the order starts being prepared — handy for sticking on cups, bags, or trays so each item is clearly tagged. It carries the item and order number, not a price or barcode.

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, followed by your full **address block** — address lines, city, region, postal code, phone, email, and tax ID — each line printing only when your store has it (no blank rows).
- **Footer**: sales receipts close with a small **"Powered by Fuze Store"** provider line.
- **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.

## Cash drawer

Each printer has a **Cash drawer** capability you can turn on (in the printer's settings) when a cash drawer is wired to it. When it's on, Fuze Store sends the "open drawer" command at the **start of every print job** routed to that printer — so the drawer pops the moment a receipt prints, exactly when the cashier needs it. A drawer-test print is included, but the standalone USB/network "Test print" button intentionally does **not** kick the drawer. Leave the capability off if no drawer is connected, and nothing happens.

## 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 type** — `Dine In`, `Take Out`, `Delivery` — printed prominently so the kitchen routes it correctly.
4. The details it has: **customer**, **table**, **guests**, the **assigned staff**, the **time placed**, and the **time printed**. 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. If a ticket is printed a second time, it carries a clear **`REPRINT`** banner so the line knows it isn't a new order.

  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:

```json
{
  "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.)

## 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:

```ts
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** (`...`) 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.

## Related guides

- [Connecting a Printer](/docs/app/printer/connecting-a-printer)
- [Routing Rules](/docs/app/printer/routing-rules)
- [LAN Bridge Setup](/docs/app/printer/lan-bridge-setup)
- [Print Queue & Status](/docs/app/printer/print-queue-and-status)
- [Troubleshooting](/docs/app/printer/troubleshooting)
