PRD: QR Self-order
| Module | Sale | PRD ID | PRD-SLF-001 |
| Status | No item | FEAT | SLF · BANA-1432 |
| Epic | BANA-1325 | Plane | BANA-1562 |
| Date | 2026-07-15 | Version | v0.2 |
| Packages | @nx/sale · @nx/commerce | URD | SLF |
| Surface | Sale · POS | ||
| Owner | Hải Cao · Khoa Nguyễn · Phát Nguyễn | ||
TL;DR
- A guest at a table scans the QR, browses a menu filtered by policy (time/channel) right on their phone - no app - and submits.
- Each submission creates its own order card, but the whole table shares one order: five guests at table 5 submitting five times still produces a single table-5 order holding five cards.
- Staff confirm or reject each card on the POS; a confirmed card prints a kitchen ticket like any staff-entered order.
- None of this exists today - the whole feature is 🚧 forward-spec.
1. Context & Problem
Peak-hour table service bottlenecks at order-taking - every table waits for a free-handed waiter to write the order down before the kitchen sees anything. There is no alternative path today: no guest-facing ordering page, no per-table QR code, no menu filtered by time/channel (policy), and no receive-and-confirm flow on the POS for an order staff never typed themselves.
Three pieces this feature needs, grounded in what already exists in code:
- Tables and occupancy already exist via Table & floor allocation - each table (
AllocationUnit) is occupied through anAllocationUsage(reserved/active/success= occupied,completed/cancelled= free), and the Sale POS already has a live table view reading this exact state. SLF does not rebuild table occupancy - it attaches guest-submitted orders to the table's existing usage. - Order lifecycle (
SaleOrderStatuses:DRAFT/PROCESSING/PARTIAL/COMPLETED/CANCELLED) already exists in SaleORD. SLF still needs to define how confirming/rejecting a card maps onto this enum (see §6, §9) - that transition is new and doesn't exist yet. - Deleting a table's order currently has no guard beyond checking that the order isn't already in a terminal status - PRD-SLF-002 closes that gap independently, ahead of SLF, and defines the "guest order can only be rejected, never deleted" rule (URD-SLF-009) that this PRD must slot into rather than redefine.
Entirely missing: the guest ordering page, per-table QR codes, the policy-based menu engine (a separate plan, epic BANA-902…907, not started), and the POS confirm/reject flow for guest-submitted orders.
2. Goals & Non-Goals
Goals
- A guest scans the right table's QR and gets the menu on their phone, no app install, no login (URD-SLF-001).
- The menu shown matches the policy in effect for the current time/channel (URD-SLF-002).
- A guest builds a cart and submits it for their table; each submission is its own card, and multiple submissions/guests at the same table fold into one order for that table (URD-SLF-003, URD-SLF-008).
- The POS receives each card tied to the right table; staff confirm or reject each card - no delete action (URD-SLF-004).
- A confirmed card prints a kitchen ticket identically to a staff-entered order (URD-SLF-005).
- The guest sees each card's status update in real time (URD-SLF-006).
- Sold-out items are hidden or unorderable on the guest menu (URD-SLF-007).
Non-Goals
- Online payment in v1 - the guest still pays at the counter/table through staff, as today.
- Loyalty integration.
- Offline operation - both the guest page and the POS require connectivity.
- A separate order/tab per guest - not a thing; every card from a table folds into a single aggregate order, and payment is still settled per table as today (see §6 FR-004).
- The policy-based menu engine itself (authoring/editing policies, assigning by time/channel) - owned by a separate epic, BANA-902…907; this PRD only consumes an existing policy, it does not define the policy admin screens.
- The confirm-vs-delete guard and audit trail for deleting a table's order - owned by PRD-SLF-002 (URD-SLF-009); this PRD only complies with the "reject, never delete" rule that PRD sets, it does not redefine it.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Pilot venues live | 1-2 real F&B venues, August |
| Guest order → kitchen | Flows end-to-end with staff confirmation; no kitchen ticket is ever produced for a rejected card |
| Card rejection rate | Tracked per pilot venue - an abnormally high rate signals a menu/pricing mismatch or a broken confirmation flow |
| Confirmation latency | Time from a card appearing on the POS to staff confirming/rejecting it - tracked to catch a "forgotten" table |
4. Personas & Scenarios
| Persona | Goal in this feature |
|---|---|
| Guest | Order from their own phone, no app, no waiting for a free-handed waiter; trust that a submitted order is confirmed, rejected with a reason, or still pending |
| Waiter / Cashier | Receive guest orders tied to the right table on the POS, confirm or reject each submission, and keep full control before anything reaches the kitchen |
| Kitchen | Receive a ticket for a confirmed card identical to one from a staff-entered order - no new process to learn |
Core scenario: five guests at one table each scan the QR on their own phone and submit a separate order. All five submissions fold into one order for table 5, showing up as five pending cards on the POS. The waiter confirms them one by one; the kitchen receives five matching tickets, none crossed with another table.
5. User Stories
- As a guest, I scan the table QR, browse the menu, order 2 dishes, and see when they're confirmed - so I don't have to flag down a waiter.
- As a guest sharing a table with friends, each of us orders on our own phone, and I know the whole table still settles as one order.
- As a waiter, guest orders appear on my POS under the right table as individual cards; I confirm or reject each one, keeping control before anything reaches the kitchen.
- As kitchen staff, I receive a ticket for a confirmed card identical to one from a staff-entered order - I don't need to tell them apart.
6. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | A guest scans a per-table QR code and opens the menu in their phone's browser - no app install, no login. An expired or invalid QR shows a message asking them to call staff, not a stale menu or a blank error page. | 🚧 | URD-SLF-001 |
FR-002 | The guest-facing menu is filtered by the policy in effect at scan time: a time window (by day of week), a sales channel (linked to the existing SaleChannel), and a scope (whole menu / by category / by item). Items or categories outside the window or the wrong channel don't appear on the guest menu. | 🚧 - hard dependency on the policy engine (BANA-902…907), which is not started | URD-SLF-002 |
FR-003 | A guest adds/removes items and quantities in a cart, adds an optional free-text note per item, sees a running subtotal, and submits the cart for their table. An empty cart cannot be submitted. | 🚧 | URD-SLF-003 |
FR-004 | Each cart submission creates its own order card. A table has at most one aggregate order at a time; every card - whether from the same guest submitting more than once or from different guests at the same table - attaches to that aggregate order, without spawning a separate order or occupying the table a second time. | 🚧 | URD-SLF-003 · URD-SLF-008 |
FR-005 | Each card appears on the POS tied to the right table, showing its items, quantities, notes, and submission time. Staff have exactly two actions per card: Confirm or Reject (reason required) - no delete action (per the URD-SLF-009 rule, fully defined in PRD-SLF-002). | 🚧 | URD-SLF-004 |
FR-006 | Confirming a card adds its items to the table's aggregate order and creates a kitchen ticket immediately, with the same structure and station routing as a staff-entered order's ticket. | 🚧 | URD-SLF-005 |
FR-007 | The guest sees each of their cards' status update in real time: Submitted → Confirmed (→ Served, if enabled), or Submitted → Rejected with a reason - no manual refresh needed. | 🚧 | URD-SLF-006 |
FR-008 | A sold-out item or variant does not appear on the guest menu, or appears greyed-out and cannot be added to the cart. An item marked sold-out after it's already in a guest's cart but before submission is auto-removed with a notice. | 🚧 | URD-SLF-007 |
FR-009 | Within the same session (QR not expired), a guest can submit another cart at any time; each additional submission creates a new card attached to the table's aggregate order under the same rule as FR-004, with no submission-count limit in v1. | 🚧 | URD-SLF-008 |
6.1 Acceptance Criteria
- A guest scans table 5's valid QR → the menu page opens directly in the phone browser, no login/app needed; the header shows the venue name and "Table 5". 🚧
- Table 5's QR has expired → the page shows "This QR code has expired - please call staff", not a stale cached menu. 🚧
- Outside the active policy's time window or on the wrong channel → the matching item/category doesn't appear on the guest menu (distinct from sold-out - no separate badge, the item simply isn't on the menu at that moment). 🚧 - depends on the policy engine, not yet built
- A guest adds 3 items to the cart and submits → a new card appears on the POS tied to table 5, listing all 3 items, quantities, and the submission time. 🚧
- Five different guests at table 5 each scan the QR and submit their own cart → five separate cards appear on the POS, all attached to the same aggregate order for table 5 - not five separate orders, not five new table occupancies (
AllocationUsage). 🚧 - A guest submits a second cart within the same session → a new card is added to table 5's aggregate order; the existing aggregate order and table occupancy are not recreated. 🚧
- Staff confirm a card → its items are added to the table's aggregate order, a kitchen ticket is created immediately, and the guest sees the card's status change to "Confirmed" without reloading the page. 🚧
- Staff try to reject a card without entering a reason → the system blocks it, requiring a reason before the rejection is confirmed; the guest is notified "Rejected" with the reason (the full confirm/reject/audit behaviour is defined in PRD-SLF-002, URD-SLF-009 - not redefined here). 🚧
- An item in a guest's cart is marked sold-out before they submit → it's auto-removed from the cart with a notice; if the cart is empty afterward, the submit button is disabled. 🚧
7. Non-Functional Requirements
| Aspect | Requirement |
|---|---|
| Performance | The guest page renders the menu within ≤3 seconds at p95 on a slow connection (~150kbps, roughly weak 3G) - 🚧 proposed figure, to be confirmed after real measurement at a pilot venue |
| QR security | The per-table token is a random string of at least 128 bits, not derivable from the table number or scan time; a 12-hour TTL is proposed (covering one shift) before it must be rescanned - 🚧 proposed figures, pending Design (July) and a security review |
| Tenancy & authz | The guest page only reads the menu/policy of the merchant and table encoded in its token; it has no write access beyond submitting a cart for that table |
| Real-time | Card status (confirmed/rejected) pushes to the guest page and every POS device over the same real-time channel already used for the live table view (Table & floor allocation), no manual refresh needed |
| i18n | The guest menu and confirm/reject copy are bilingual (EN + VI), following the merchant's preferred language |
8. UX & Flows
One guest orders, gets confirmed, reaches the kitchen
Multiple guests, one table - one order, many cards
Key surfaces: the guest page (menu → cart → status) runs entirely in the phone browser, no install; the POS gains a "pending" area per table where each card is its own row with Confirm/Reject buttons - no delete icon on a guest-submitted card.
9. Data & Domain
| Concept | Role |
|---|---|
AllocationUnit / AllocationUsage | The table and its occupancy, already provided by Table & floor allocation; SLF builds no new occupancy mechanism, it only attaches guest orders to a table's existing usage (reserved/active/success = occupied) |
SaleOrder (table's aggregate order) | The existing order entity, carrying SaleOrderStatuses (DRAFT/PROCESSING/PARTIAL/COMPLETED/CANCELLED); a table has at most one open aggregate order for the guest-ordering flow at a time |
| Order card (new, conceptual) | One guest cart submission; multiple cards attach to the same table aggregate order, each carrying its own state (pending / confirmed / rejected with reason) independent of the aggregate order's status |
| Per-table QR token (new) | The identifier used to open the guest page, carrying an expiry (TTL) - see NFR |
| Menu policy (out of scope, consumed via API) | Determines which items/categories show by time/channel; owned by epic BANA-902…907, not started - this PRD only consumes its output, it does not define policies |
Conceptual only - "order card" and "QR token" are proposed models; the real schema is pending Design/BE in July-August.
10. Dependencies & Assumptions
Depends on
- Policy-based menu engine (BANA-902…907) - it's the menu the guest sees (FR-002). 🚧 Not started.
- Table & floor allocation -
AllocationUsageis the source of truth for which table is open; guest orders attach to it. ✅ Built, except FR-006 (tier/room/table/seat quantity caps), which is currently 🔶 partial. - PRD-SLF-002 (table-order delete guard) - defines the "guest card/order can only be rejected, never deleted" rule (URD-SLF-009) that this PRD's FR-005 must slot into. 🚧 Planned, may ship ahead of SLF.
- Order lifecycle (
SaleOrderStatuses, ORD) - confirming/rejecting a card must map onto the existing order status enum. ✅ Built. - Design (July) - guest UI + POS confirm/reject UX, plus the NFR figures still 🚧 (token TTL, page-load SLA). Not started.
Assumptions
- At most one live
AllocationUsageoccupies a given table at a time (per the FLR model), so "which table's aggregate order" is always unambiguous when folding in multiple cards. - Once complete, the policy engine (BANA-902…907) exposes an API to read "available menu by merchant + time + channel" that SLF consumes directly, without re-implementing the filtering logic.
- A pilot venue has enough staff on hand to confirm/reject cards within a reasonable time - this PRD does not set a hard response-time SLA in v1 (see Risks).
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Abuse/garbage orders from guests | Staff confirmation on every card is the v1 guard - no card reaches the kitchen without it |
| Unlimited submissions per session (FR-009) could be abused for spam | No hard limit in v1; monitored via the "card rejection rate" metric (§3), revisit if pilot operations surface a real problem |
| QR token TTL and page-load SLA figures aren't finalized | Proposed values live in §7 NFR; pending Design (July) and real pilot measurement to confirm |
| Staff never respond to a pending card | Out of scope for v1 - no SLA or auto-reminder yet; add in a future increment if pilot operations need it |
| The policy engine (BANA-902…907) hasn't started and may slip past SLF's July-August milestones | FR-002 is a hard dependency; if policy slips, the guest menu could temporarily run unfiltered (showing the whole menu) as a fallback - Design must sign off on this before it's built |
12. Release Plan & Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (Jul-Aug) - SLF in the URD feature catalog |
| Rollout | Pilot F&B venues first (1-2, August), wider rollout once launch criteria pass |
| Migration | None needed - the table's aggregate order reuses the existing SaleOrder/AllocationUsage model; order cards are additive new data |
| Launch criteria | AC-SLF-01 (URD) passes at a pilot venue: a guest submits a 2-item order → POS shows it pending on the right table → confirming it sends a ticket to the kitchen; sold-out items are unorderable; multiple guests at one table correctly fold into one order with multiple cards |
| Monitoring | Card rejection rate, confirmation latency by table/venue, guest-page load failure rate (expired/invalid QR), average submissions per table per session |
13. FAQ
- Does the guest pay online? Not in v1 - ordering only; payment stays with staff at the counter/table as today.
- What happens when multiple guests at one table order at the same time? Each submission creates its own card, but every card from that table folds into one aggregate order - there's no separate tab per guest, and payment is still settled per table (see FR-004).
- Can staff delete a guest card submitted by mistake? No - the only removal action is reject, with a required reason; delete never applies to a guest-submitted order/card, per the rule in PRD-SLF-002 (URD-SLF-009).
- What if the policy-based menu engine isn't ready when SLF is due to build? FR-002 hard-depends on BANA-902…907; if it slips, Design must sign off on a fallback (e.g. showing the whole menu unfiltered) before build - the filtering step is never silently skipped.
References
- URD: Sale - SLF
- Related PRDs: PRD-SLF-002 - Prevent accidentally deleting a table's queued order · PRD-FLR-001 - Table & floor allocation
- Module: Sale - URD · Roadmap: Phase 2 - By Month
- Developer: @nx/sale · domain model
- Epic BANA-1325