PRD: Prevent accidentally deleting a table's queued order
| Module | Sale | PRD ID | PRD-SLF-002 |
| Status | Ready to dev | FEAT | SLF · BANA-1432 |
| Epic | BANA-1325 | Plane | BANA-1750 |
| Date | 2026-07-06 | Version | v0.1 |
| Packages | @nx/sale · @nx/commerce | URD | SLF |
| Surface | Sale · POS | ||
| Owner | Tinh Nguyễn | ||
TL;DR
Today, deleting a sale order only checks that it isn't COMPLETED/CANCELLED - it does not check whether the order is still occupying a table. A misclick on a busy table's order silently soft-deletes it with no confirmation and no way back through the UI. This PRD adds a table-aware confirmation gate to the delete path - naming the table and what's on the order before it disappears - and makes the guard mandatory (not just a warning) for a guest-submitted order still awaiting staff confirm/reject once QR self-order ships.
1. Context & Problem
A dine-in order isn't just a row in the cart - while it's open (DRAFT/PROCESSING/PARTIAL), it usually holds a live table occupancy (AllocationUsage, status reserved/active/success) via Table & floor allocation. The existing delete endpoint (DELETE /sale-orders/:id, backed by SaleOrderService.archive()) only refuses terminal orders (COMPLETED/CANCELLED) - it happily soft-deletes a DRAFT, PROCESSING, or PARTIAL order with no regard for whether a table is actively occupied by it, and with no confirmation step beyond whatever (if any) the client shows.
In practice this means a waiter clearing an empty table, or fat-fingering the wrong row in a busy table list, can wipe out a queued order - one that's sitting on an occupied table, not yet checked out or served - with items the guest is still waiting on, and no staff-facing trace beyond the ORDER_DELETED socket event and a deletedAt timestamp in the DB. There is no in-product way to recover it.
This risk is about to get worse, not better: QR self-order (SLF, Planned) will land guest-submitted orders directly onto a table's pending list awaiting staff confirm/reject. A guest order a waiter never typed is more likely to be swiped away by habit ("clear this table") before anyone reads it - and unlike a staff-entered order, the guest has no idea it happened. This PRD closes the gap now, ahead of and independent from SLF shipping, and defines the stricter rule SLF needs on day one: a pending guest order can be rejected (a first-class, guest-visible action) but never silently deleted.
2. Goals & Non-Goals
Goals
- Any delete of a non-terminal order that is currently occupying a table requires an explicit confirmation naming the table and summarizing what's on the order (item count, elapsed time) before it proceeds.
- A guest-submitted order (SLF) awaiting confirm/reject can never be deleted directly - only rejected, a distinct, audited, guest-visible action.
- Every confirmed delete of a table-linked order is audit-logged (who, when, which table, an order/item snapshot) so a wrong delete can be investigated after the fact.
- The guard applies uniformly to single-order delete and any bulk/multi-select delete action - no back door.
Non-Goals
- Restoring a deleted order from the UI ("undo delete") - the row is already soft-deleted (
deletedAt) today; a restore surface is a future increment, not this one. - Changing the cancel lifecycle transition (
PROCESSING/PARTIAL→CANCELLED) - cancel already exists as a distinct, non-destructive path and is out of scope here. - The guest-order confirm/reject flow itself, or the rest of QR self-order (menu, cart, kitchen hand-off) - owned by
PRD-SLF-001; this PRD only adds the delete guard those flows must respect. - Reworking table/floor occupancy modeling - consumed as-is from Table & floor allocation.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Accidental table-order deletions | Support-reported "lost order" incidents trend to zero post-rollout |
| Confirmation coverage | 100% of deletes on a table-occupied, non-terminal order show the table-aware confirmation - no code path bypasses it |
| Guest order safety | 0 guest-submitted orders removable by delete; reject is the only removal path once SLF ships |
| Auditability | 100% of confirmed table-order deletes have a retrievable audit entry (who / when / table / snapshot) |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Waiter / Cashier | Clear an empty table's stale draft without fear of wiping a busy table by mistake |
| Guest (via SLF, once shipped) | Trust that a submitted order is either confirmed, rejected with a reason, or still pending - never silently gone |
| Manager | Investigate a "my order disappeared" complaint using an audit trail instead of guesswork |
Core scenario: a waiter taps delete on table 5's order meaning to clear table 2 (empty). Today this succeeds instantly. With this PRD, they see "Delete order for Table 5 - 3 items, opened 12 min ago? This cannot be undone" and must confirm - catching the mistake before it happens.
5. User Stories
- As a waiter, I want a confirmation that names the table and what's on it before a delete goes through, so I don't lose a busy table's order to a misclick.
- As a manager, I want every table-order delete logged with who/when/what, so I can investigate a lost-order complaint instead of guessing.
- As a guest ordering via QR, I want my submitted order to only ever be confirmed or explicitly rejected - never silently erased - so I know what happened to it.
6. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | Deleting a non-terminal order (DRAFT/PROCESSING/PARTIAL) that has a live AllocationUsage (an occupied table) requires an explicit confirmation step before the delete proceeds | 🚧 | URD-SLF-009 |
FR-002 | The confirmation names the table/unit and summarizes the order (item count, time since opened) so staff can recognize what they're about to lose | 🚧 | URD-SLF-009 |
FR-003 | A guest-submitted order awaiting staff confirm/reject cannot be deleted through any path; the only removal action available is reject, distinct from delete, requiring a reason and visible to the guest | 🚧 | URD-SLF-009 · URD-SLF-004 |
FR-004 | The guard applies to bulk/multi-select delete actions identically to single-order delete - no action clears a table-linked order without the same confirmation | 🚧 | URD-SLF-009 |
FR-005 | Every confirmed delete of a table-linked order writes an audit entry: actor, timestamp, table/unit, and a snapshot of the order (items, status) at delete time | 🚧 | URD-SLF-009 |
FR-006 | Deleting an order with no live table occupancy (e.g. a walk-in draft never assigned a table) is unaffected - the existing terminal-status check is the only guard, no new confirmation required | ✅ | URD-SLF-009 |
6.1 Acceptance Criteria
- A
DRAFTorder occupies table 5 (liveAllocationUsage) and staff tap delete → a confirmation names table 5 and summarizes the order (item count, time since opened); the delete only proceeds on confirm. - A guest-submitted order on table 5 is awaiting confirm/reject and staff try to remove it → no delete action is offered - only Confirm / Reject; reject requires a reason and the guest is notified.
- An order has no live table occupancy (an unassigned walk-in draft) and staff delete it → it is removed under the existing terminal-status check alone, no extra confirmation shown.
- Staff select a batch of orders that includes one occupying a busy table and choose bulk delete → the table-occupied order is held back for the same confirmation while the rest of the batch proceeds - no bulk action bypasses the guard.
- Staff confirm the delete of a table-linked order → an audit entry is written with the actor, timestamp, table/unit, and an order snapshot, retrievable afterward for investigation.
- A table's usage completes (the order is paid) between the client showing "occupied" and the delete request landing → the guard re-checks live occupancy at request time, so the now-free table's delete proceeds without a stale block.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | The guard reads the order's live AllocationUsage state at delete time (not a cached/stale flag) so a just-vacated table doesn't block a legitimate delete |
| Tenancy & authz | Confirmation and audit entries are merchant-scoped; delete stays gated by the existing archive/delete permission - this adds a guard in front of it, not a new permission tier |
| Auditability | Audit entries are append-only and queryable by order, table, and actor for support investigation |
| Performance | The occupancy check adds at most one indexed lookup to the existing delete path - no perceptible latency on the POS |
| i18n | Confirmation copy and reject-reason prompts are bilingual (EN + VI) |
8. UX & Flows
Delete blocked by table occupancy
Guest-submitted order (once SLF ships) - reject, never delete
Key surfaces: the table order list and the individual order view both gain the confirmation dialog wherever a delete action exists today; the guest-order pending card exposes Confirm/Reject only - no delete icon.
9. Data & Domain
| Entity | Role |
|---|---|
SaleOrder | The order being deleted; carries status and deletedAt (soft delete) |
AllocationUsage | Polymorphic occupancy row binding a table AllocationUnit to this order; reserved/active/success = occupied, completed/cancelled = free - read at delete time to decide whether the guard fires |
| Deletion audit entry (new) | Append-only record: actor, timestamp, table/unit, order snapshot - written on every confirmed table-linked delete |
Conceptual only - full schema in the sale domain model and Allocation Usage.
10. Dependencies & Assumptions
Depends on
- Sale order lifecycle (ORD) - the existing
archive()/delete path this PRD adds a guard in front of. - Table & floor allocation (commerce/FLR) -
AllocationUsageis the source of truth for "is this table occupied." - QR self-order (SLF, Planned) - defines the confirm/reject flow this PRD's reject-not-delete rule must slot into once built.
Assumptions
- The delete guard can ship ahead of SLF - it protects today's staff-entered table orders immediately; the guest-order reject-only rule activates as soon as SLF's confirm/reject surface exists.
- At most one live (non-terminal)
AllocationUsageoccupies a given table at a time (per the FLR model), so "is this table occupied by this order" is an unambiguous check.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Confirmation fatigue - staff click through every dialog without reading | Confirmation copy leads with the table name and item count, not a generic "Are you sure?"; revisit copy/friction level after pilot feedback |
| A table's usage ends (order paid) between the client rendering "occupied" and the delete request landing | Guard re-checks live AllocationUsage state at request time, not a client-cached flag (NFR) |
| Staff need to remove a stale/duplicate guest order that was never going to be confirmed | Covered by FR-003 - reject (with reason) is the sanctioned removal path, not delete |
| Should a manager-level override bypass the confirmation for bulk table clears (e.g. end of service)? | Open - default is no bypass in v1; revisit if operations raises a real need |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - ships as a guard on the existing ORD delete path; the guest reject-only rule activates with SLF |
| Rollout | All merchants; no feature flag - this is a safety fix, not an optional behavior |
| Migration | None - reads existing AllocationUsage state; the audit entry is an additive record |
| Launch criteria | Deleting a table-occupied, non-terminal order always shows the table-aware confirmation and is blocked without it; a guest-submitted pending order has no delete action, only confirm/reject; every confirmed table-order delete has a retrievable audit entry |
| Monitoring | Confirmation-shown vs. confirmed-delete ratio (catches near-misses), audit-entry volume by table/actor, any delete request that bypassed the guard (should be zero) |
13. FAQ
Does this change how orders are cancelled? No - cancel (PROCESSING/PARTIAL → CANCELLED) is a separate, already-existing transition and isn't touched here. This PRD only guards the destructive delete/archive action.
Can staff still delete an empty table's stray draft quickly? Yes - an order with no live table occupancy isn't gated by this guard at all (FR-006); only table-occupied orders require the confirmation.
What happens to a guest's order if staff never respond? Out of scope for this PRD - covered by QR self-order's confirm/reject timing rules. This PRD only guarantees that whatever happens, it's confirm or reject, never a silent delete.
Can a wrongly-deleted order be restored? Not through the UI in v1 - the row is soft-deleted (deletedAt), so the data exists, but restore tooling is a future increment (see Non-Goals).
References
- URD: Sale - SLF
- Related PRD: QR self-order · Table & floor allocation
- Module: Sale - URD
- Developer: @nx/sale · domain model · Allocation Usage