Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ Both scripts ask whether to keep application data. Do not choose their data-purg
- **Customer loyalty & CRM:** Customer directory, purchase histories, and point redemption.
- **Cash drawer management:** Cash pulse on checkout and end-of-shift cash drawer reconciliations.
- **Daily sales & accounting export:** Audit-ready CSV/Excel reporting with financial reconciliation identities.
- **Multi-counter ordering:** Separate order capture, cashier payment, or combined counter terminals with shared LAN queue and QR ticket lookup.

## Project status

Expand Down
15 changes: 8 additions & 7 deletions docs/architecture/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,16 +646,17 @@ frontend/src/
│ ├── kds/ Kitchen display, login, tabs and kanban views
│ ├── layout/ Title bar, sidebar, status bar, auth guard, theme sync
│ ├── orders/ Order card, history grid, refund modal
│ ├── pos/ Cart, product grid, payment, number pad, checkout modals
│ ├── pos/ Cart, product grid, payment, number pad, checkout modals,
│ │ payment queue drawer, payment ticket modal
│ ├── products/ Product image uploader
│ ├── providers/ I18nProvider
│ ├── settings/ The settings screen's building blocks:
│ │ BetaChannelToggle, ChargesSettingsCard, CurrencyResetDialog, DatabaseSettingsTab,
│ │ GeneralSettingsTab, HealthCheckDialog,
│ │ InitializeDatabaseDialog, LocalePreferencesPanel, MasterPinPrompt,
│ │ PaymentMethodsSettings, PermissionAuditLog, PermissionMatrix,
│ │ PrintersSettingsTab, SettingsTabShell, TaxConfigurationPanel,
│ │ Toggle, WhatsAppEnableCard
│ │ BetaChannelToggle, ChargesSettingsCard, CountersSettingsTab,
│ │ CurrencyResetDialog, DatabaseSettingsTab, GeneralSettingsTab,
│ │ HealthCheckDialog, InitializeDatabaseDialog, LocalePreferencesPanel,
│ │ MasterPinPrompt, PaymentMethodsSettings, PermissionAuditLog,
│ │ PermissionMatrix, PrintersSettingsTab, SettingsTabShell,
│ │ TaxConfigurationPanel, Toggle, WhatsAppEnableCard
│ ├── support/ The support hub: SupportTicketForm, DiagnosticsPanel
│ ├── tables/ Floorplan editor, table move modal, table turnover badge
│ ├── ui/ shadcn/ui primitives
Expand Down
60 changes: 54 additions & 6 deletions docs/architecture/order-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,49 @@ Lifecycle rules before and at release:
collected funds requires refunding the collected amount in full through `/api/refunds` first (`409`
with `REFUND_REQUIRED_BEFORE_CANCEL`), preserving bills and audit history.

## Multi-counter ordering (create here, pay there)

FloCafe supports owner-configured multi-counter ordering (`multi_counter_enabled`, disabled by
default). In this workflow, separate physical terminals act as ordering stations, payment cashiers,
or combined stations:

- **Named counters and roles.** The store owner defines counters with roles: `ordering` (create order
and send to payment), `payment` (manage awaiting payment queue and take tender), or `combined`
(both standard checkout and send to payment). Each POS terminal selects its active counter,
remembered locally on that device scoped to the active store.
- **Workflow guidance without ownership gating.** Counter roles guide UI defaults without restricting
underlying staff permissions: ordering counters default to order building and 'Send to payment';
payment counters automatically open the shared payment queue and orient the cart CTA toward the
queue; combined counters retain standard direct checkout while allowing explicit 'Send to payment'.
Staff can view and act on any order across counters; orders are never ownership-gated.
- **Real orders and bills.** "Send to payment" creates real `orders` and `bills` records, never
transient held-order JSON. Orders record `dispatch_policy = 'on_payment'` and start in
`checkout_stage = 'awaiting_payment'`.
- **Supported order types.** Multi-counter checkout is supported for **takeaway and dine-in** orders.
Delivery orders cannot use "Send to payment" (`400`). Takeaway is the order type that can combine
both multi-counter checkout and scheduled kitchen release.
- **Kitchen suppression prior to release.** Orders with `dispatch_policy = 'on_payment'` set
`release_status = 'scheduled'` and defer stock deduction, KDS display, active-order heartbeats, and
automatic KOT printing until full payment is collected.
- **Shared transactional release gate.** When full payment across all bills settles:
- If unscheduled, the order is atomically released to the kitchen via `releaseScheduledOrder`.
- If scheduled for a future instant, `checkout_stage` transitions to `'paid'`, while
`release_status` stays `'scheduled'` until the scheduled instant arrives (or manual release is
triggered).
- Manual release (`POST /api/orders/:id/release`) on an unpaid `on_payment` order fails with `409`
(`ORDER_NOT_PAID`).
- Prepayment does not bypass kitchen preparation (orders with unserved kitchen items keep
`orders.status = 'pending'`) and does not clear occupied dine-in tables.
- **Paid-but-blocked tickets.** If catalog preconditions or inventory levels fail at release time
after payment, the transaction atomically transitions `release_status = 'blocked'` with
`release_blocked_reason` without reverting the committed payment. The ticket remains clearly
visible in the payment queue for explicit staff retry after restocking or refund/cancellation.
- **Shared LAN queue and QR ticket identification.** Terminals retrieve tickets via
`GET /api/orders/awaiting-payment` (cursor-paginated with counts) or lookup by ticket/order number
via `GET /api/orders/by-ticket/:ticket`. Tickets display a human-readable number and QR code
(`FLOCAFE-TICKET:<order_number>`), which carries zero authentication or payment authority, and can
be printed as a thermal ticket slip (`POST /api/printers/print-payment-ticket`).

## Discounts

Three surfaces apply discounts, all gated by the discount permission for their scope and by the same
Expand Down Expand Up @@ -299,12 +342,17 @@ its amount via `PATCH /api/bills/:id/payments/:index/method` (see [the Bills API
## Settlement and completion

A bill moves `unpaid` to `partial` to `paid`; refunds move it to `partially_refunded` or `refunded`.
When a payment settles the last unpaid bill of an order, an immediate or already-released order
becomes `completed` (unless it was already completed or cancelled) and its dine-in table is released
to `available`; an unreleased scheduled order remains `pending` at payment time, then completes on
release when KDS is disabled (or no active unserved items exist) or when its last active kitchen
item transitions to `served` while KDS is enabled, provided the order remains settled and
non-refunded. A payment never resurrects a terminal order.
When a payment settles the last unpaid bill of an order with immediate dispatch
(`dispatch_policy = 'immediate'`), an already-released order becomes `completed` (unless it was
already completed or cancelled) and its dine-in table is released to `available`. Prepayment under
multi-counter ordering (`dispatch_policy = 'on_payment'`) preserves `orders.status = 'pending'` and
keeps occupied dine-in tables occupied: completing payment transitions `checkout_stage = 'paid'` and
triggers release to the kitchen (if unscheduled or due), but the order completes only after food
service concludes. An unreleased scheduled or multi-counter order remains `pending` at payment time,
then completes on release when KDS is disabled (or no active unserved items exist) or when its last
active kitchen item transitions to `served` while KDS is enabled, provided the order is not a dine-in
order with an active table and remains settled and non-refunded. A payment never resurrects a terminal
order.

When a discount, an order mutation (`syncUnpaidBillsForOrder`), or a `POST /api/bills/generate`
re-sync reduces the remaining balance of a partially paid bill to exactly zero (where collected funds
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture/printing.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,11 @@ four whitespace-separated tokens read as a sentence.
| WebUSB | `frontend/src/lib/printer/PrinterService.ts` | Encoder output, sent over the browser's WebUSB API. |
| Delivery slip | `frontend/src/lib/printer/delivery-slip-encoder.ts` and `delivery-slip-web-print.ts` | A `DeliverySlipDocument`'s contact facts, rendered as bytes or HTML. |
| Tax bill | `frontend/src/lib/printer/tax-bill-encoder.ts` | A raw `Bill` and `Tenant`, not a document. |
| Payment ticket slip | `printPaymentTicket` in `main/printers/thermal.ts` | Order metadata, item lines, balance due, and ESC/POS QR code (`FLOCAFE-TICKET:<order_number>`) for multi-counter ordering. |

The tax bill encoder is the single exception to "renderers consume documents". It targets the
detailed per-component tax receipt, a different receipt shape from `PrintDocument` v1, and reads
the bill directly.
The tax bill encoder and payment ticket slip generator are exceptions to "renderers consume
documents": the tax bill encoder reads `Bill` directly for per-component tax detail, while
`printPaymentTicket` formats a compact payment ticket slip and QR code from order items and totals.

### Browser HTML printing

Expand Down
Loading
Loading