PRD: Table & floor allocation
| Module | Commerce | PRD ID | PRD-FLR-001 |
| Status | Ready to dev | FEAT | FLR |
| Epic | — | Plane | BANA-1532 |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/commerce · @nx/sale · @nx/core | URD | FLR |
| Surface | Client · Owner/MgrSale · POS | ||
| Owner | Phát Nguyễn | ||
What table & floor allocation is
A dine-in merchant models its premises as a named floor layout (Layout), holding a tree of zones (Zone) nested at most two levels deep; each leaf zone owns tables (Unit) carrying their own capacity, canvas placement, and style. The owner or manager builds this tree on the Client page. When a server opens a dine-in order on the Sale POS, the system records an occupancy record (Usage) binding that order to one or more tables - this record drives the colour of every tile on the floor plan and is the source of truth for availability, transfer, split, and merge. Every order also keeps a snapshot of its table and guest details, so the assignment survives even if the live usage record later changes.
Why design is kept apart from occupancy
Two distinct jobs hide behind "tables". One is design - laying out the physical space, done on the Client page, an infrequent task run by the owner or manager. The other is occupancy - which table a live order is on, right now, run on the Sale POS, a high-frequency, real-time concern handled by servers or cashiers. Modelling them as one editable blob would make the floor plan churn on every seating; managing occupancy without a stable plan would leave orders pointing at nothing.
Without this map, a server cannot see at a glance which tables are open, the kitchen has no anchor for a dine-in order, and "move table 4 to the patio" is a verbal handoff with nothing recorded.
One shift, from design to freeing the table
An owner designs "Ground Floor" on the Client page as a layout with two rooms, each holding several tables, in one save. During service a server on the Sale POS opens a dine-in order on Table 7; the tile turns busy on every terminal. A walk-in of six arrives - the server filters for free tables seating six, seats them, then later transfers them from the main room to the patio in one action. When each party leaves, completing the order's usage frees the table back to green.
The easiest thing to get wrong
The floor plan and table occupancy are not the same thing
The two change at completely different rates. The plan is a rare edit on the Client page; occupancy churns every seating on the Sale POS. Keeping them apart means the live floor never rewrites the plan, and orders always point at a stable table.
1. Goals & Non-Goals
Goals
- Provide a floor-plan design screen on the Client page (drag-and-drop zones and tables, capacity, placement, and style) for the owner or manager.
- Model a merchant's physical floor as a named layout → zone → unit tree, with each unit carrying capacity, canvas placement, and style.
- Author the whole tree in one atomic save (create, update, delete together), with a depth cap and a quantity cap per floor/room/table/seat, and a check that every zone is rooted in its layout.
- Occupy units by opening a dine-in sale order on the Sale POS; drive a live floor plan that shows each unit free or busy.
- Find free tables - available units in a zone, and available child zones where every table is free.
- Transfer a party between zones, and keep allocations correct when an order is split or merged.
- Broadcast a real-time floor update on every occupancy change; keep an allocation snapshot on the order.
Non-Goals
- Reservations - specified in a separate PRD, out of scope for this PRD.
- Pricing or service charge per zone/table - prices live in pricing; allocation carries no money.
- KDS / kitchen routing and the order lifecycle itself (Sale) - allocation references an order, it does not own it.
2. Success Metrics
| Metric | Target / signal |
|---|---|
| Floor integrity | A failed sub-step in a save leaves no half-built tree - layout, zones, and units are saved together or not at all |
| Depth & quantity safety | No save ever persists a zone tree deeper than the two-level cap, a zone not rooted in its layout, or a floor/room/table/seat count over its cap |
| Occupancy truth | A unit shows busy exactly while it has a reserved/active/success usage, and free otherwise |
| Availability accuracy | "Free tables" returns only units with no live usage; a zone is offered only when all its tables are free |
| Live sync | Every occupy / free / transfer reflects on all Sale POS terminals without a manual refresh |
3. Personas & Scenarios
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | On the Client page, lay out the floor once - zones, tables, seats, positions - and edit it as the space changes |
| Server / Cashier | On the Sale POS, see which tables are free, seat a party, move it, and free the table when guests leave |
| Host | On the Sale POS, read the live floor at a glance and pick an open table of the right size |
Core scenario: an owner designs "Ground Floor" on the Client page as a layout with two rooms, each holding several tables, in one save. During service a server on the Sale POS opens a dine-in order on Table 7; the tile turns busy on every terminal. A walk-in of six arrives - the server filters for free tables seating six, seats them, then later transfers them from the main room to the patio in one action. When each party leaves, completing the order's usage frees the table back to green.
4. User Stories
| # | As a | I want | So that |
|---|---|---|---|
| 01 | owner | on the Client page, to design my floor as zones and tables in one save | the map is complete and consistent the moment it exists |
| 02 | owner | on the Client page, to edit the layout - add a room, remove a table, reposition seats - in one save | I do not have to rebuild it from scratch |
| 03 | server | on the Sale POS, to see free vs busy tables live | I never seat a party on an occupied table |
| 04 | server | on the Sale POS, to open an order on a table and have it immediately show busy to everyone | the floor is never double-sold |
| 05 | server | on the Sale POS, to transfer a seated party to another zone in one action | the old table frees while the new one fills |
| 06 | server | on the Sale POS, to free a table when guests leave and have it turn free again | the next party can be seated |
5. Functional Requirements
| # | Requirement | Status | URD ref |
|---|---|---|---|
FR-001 | A merchant models its floor as a named layout holding a tree of zones. Leaf zones own units with capacity, placement, and style. | ✅ | URD-FLR-001..003 |
FR-002 | A layout is created with its whole nested zone tree in one atomic save. | ✅ | URD-FLR-004 |
FR-003 | A layout save follows an id rule per item in the tree. • Sent with only an id, it is removed (cascading its sub-tree). • Sent with an id and details, it is updated. • Sent with no id, it is created. | ✅ | URD-FLR-005 |
FR-004 | A zone is itself create/update-able in one save with nested sub-zones and units. Zones can be created in batches. | ✅ | URD-FLR-006 |
FR-005 | Depth is capped at two levels below the layout root. A deeper tree, or a zone not rooted in its layout, is refused before it is saved. | ✅ | URD-FLR-007 |
FR-006 | A floor-plan save is capped by quantity: at most 100 floors per layout, 50 rooms per floor, 50 tables per room, 50 tables per zone. • Each table declares a capacity of at most 50 seats. • Exceeding any cap is refused with a clear message. | 🔶 | URD-FLR-022 · URD-FLR-023 |
FR-007 | A layout is readable with a configurable max depth - full tree for the Client page, shallow for the Sale POS. | ✅ | URD-FLR-008 |
FR-008 | Layouts, zones, and units carry a lifecycle status (Activated / Deactivated / Archived) and are removed recoverably. Each is also managed standalone. | ✅ | URD-FLR-009..010 |
FR-009 | Opening a dine-in sale order on one or more units creates an occupancy usage that marks those units busy. | ✅ | URD-FLR-011 |
FR-010 | A usage carries a reservation window; with no end given, a default 90-minute window applies. | ✅ | URD-FLR-012 |
FR-011 | Occupancy follows reserved/active → success → completed (freed) or cancelled. A usage already completed or cancelled silently ignores any further complete/cancel command - no state change, no error. | ✅ | URD-FLR-013 |
FR-012 | A unit is busy while it holds a reserved/active/success usage and free otherwise. The Sale POS can query free units and fully-free child zones. | ✅ | URD-FLR-014..015 |
FR-013 | A party can be transferred between zones - source usages cancelled, the target zone's units occupied per order, atomically. | ✅ | URD-FLR-016 |
FR-014 | Splitting an order clones its active usages onto each new order. Merging moves them to the surviving order. | ✅ | URD-FLR-017 |
FR-015 | Every occupancy change broadcasts a real-time floor update to all Sale POS terminals. | ✅ | URD-FLR-018 |
FR-016 | A usage may capture guest details. The order keeps an allocation snapshot (the unit, its zone path, and guest details). | ✅ | URD-FLR-019..020 |
FR-017 | All allocation operations are confined to the user's own merchant and gated by allocation permissions. | ✅ | URD-FLR-021 |
5.1 Acceptance Criteria
- Save a new layout with a nested zone and unit tree in one submit → the layout, zones, and units are all created together.
- Within the same save, send a zone with only its id (no details) → that zone and its whole sub-tree are removed.
- Send a zone with both an id and details → the zone is updated with the new details; send a zone with no id → a new zone is created.
- A sub-step fails mid-save (for example a unit with bad data) → the whole save rolls back, no zone or unit is left half-created.
- Save a zone tree deeper than two levels below the layout root → refused before anything is saved.
- Save a zone that references the wrong layout (not its own root layout) → refused for not being rooted in its layout.
- Add a 51st table to a room and save → refused with a clear message.
- Declare a table with 60 seats and save → refused.
- Read a layout on the Client page → get back the full tree; read the same layout from the Sale POS → get back a shallow tree at the configured depth for the point of sale.
- Delete a layout, zone, or unit → it moves to Archived, is recoverable, and no data is lost.
- Open a dine-in order on a free Table 7 → the unit turns busy on every Sale POS terminal, and a usage is recorded with a reservation window (default 90 minutes if no end time is given).
- Complete a table that has a paid (success) order → the usage becomes completed and the unit turns free again.
- Free a table a second time for an already-closed seating → nothing changes, no error, other tables in the same batch still process normally.
- Query free tables in a zone that has several busy tables → only units with no reserved/active/success usage come back.
- Query available child zones of a parent zone → only child zones where every table is free come back.
- Transfer a party seated in the main room to the patio zone → the source usage is cancelled, the patio's unit is occupied for each affected order, and both tiles change colour on every terminal right away.
- Attempt a transfer when the source zone has no active usage, or the target zone has no unit → refused with a clear reason, nothing changes.
- Split an order with an active usage → each new order gets its own copy of the usage; merge two orders with active usages → all usages move to the surviving order.
- Open, transfer, or free a table → every other Sale POS terminal gets the tile colour update immediately, no manual refresh needed.
- Open an order recording the guest's name, phone, and party size on a table → the guest details are saved with the usage.
- Look up an order after its usage has since changed → the order still holds the correct table and zone path via its own allocation snapshot.
- A staff member without allocation permission, or logged into a different merchant, attempts to open/transfer/free a table → refused; they only see the plan and tables of their own merchant.
6. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Atomicity | The floor model (layout + zones + units) and each occupancy operation (start, transfer, split, merge) are all-or-nothing |
| Depth & quantity safety | A check rejects a zone tree deeper than two levels, zones not rooted in their layout, and any floor/room/table/seat count over its cap - all before anything is saved |
| Real-time | Occupancy changes push real-time updates so every Sale POS terminal reflects the floor without a manual refresh |
| Tenancy & authz | All operations confined to the user's own merchant; floor design on the Client page is gated by layout/zone/unit permissions, occupancy on the Sale POS by usage permissions |
| Performance | Availability resolves a zone's units in a single pass; the floor read caps depth so the Sale POS fetches only what it renders |
| Durability | Each order keeps an allocation snapshot (the unit, its zone path, and guest details) so the assignment survives on the order independent of the live usage records |
| i18n | Layout, zone, and unit names are bilingual (English / Vietnamese) |
7. UX & Flows
Designing the floor plan on the Client page
An owner or manager opens the floor-plan design screen on the Client page, drags to lay out floors, rooms, and tables into a tree at most two levels deep, sets each table's capacity, canvas position, and style, then saves the whole tree in one submit. Every create, edit, and delete action lives on this screen - the Sale POS has no button to create or edit the plan.
Occupancy lifecycle
Completed and Cancelled are terminal states. A complete or cancel command sent against a usage already in a terminal state is silently ignored - no state change, no error. That way, freeing a batch of tables at the end of the day never gets stuck on one table that was already closed.
Seat, see live, transfer on the Sale POS
The floor model surfaces on the Client page as a design screen (zones, tables, capacities, positions) for the owner or manager, and on the Sale POS as the live floor plan that colours each tile by occupancy and only powers seat / find-free / transfer / free-table - it does not create or edit the plan.
8. Data & Domain
| Entity | Role |
|---|---|
| Floor layout | A merchant's named floor map; root of the zone tree; carries floor-plan style |
| Zone | A region in the tree (floor / room / table grouping); self-nesting up to two levels under the layout |
| Unit (table) | A seatable unit (table); carries capacity, floor-plan placement, and style; belongs to a leaf zone |
| Occupancy record (Usage) | A live occupancy binding a unit to a sale order, with a reservation window and a status that drives the floor colour |
| Reservation | Specified in a separate PRD - out of scope for this document |
Conceptual only. Relations are loose references; integrity is enforced by the allocation rules, not by storage constraints.
9. Dependencies & Assumptions
Depends on
| # | Feature | What is depended on |
|---|---|---|
| 01 | Merchant (MER, PRD-ORG-001) | every layout, zone, unit, and usage is scoped to a merchant. |
| 02 | Sale orders (Sale) | a usage occupies units on behalf of a dine-in order; split / merge / payment drive its lifecycle. |
| 03 | The shared platform | the allocation models, statuses, and the live update channel. |
Assumptions
| # | Assumption | What breaks if it is wrong |
|---|---|---|
| 01 | The merchant's business type is dine-in (F&B); counter / retail merchants do not need a floor model. | A merchant that is not dine-in but still needs a seating layout (for example, a self-service counter with a seating area) is not given a floor model, and has to manage that layout outside the system. |
| 02 | Zone depth in practice is at most floor → room → table; the two-level cap reflects that. | A merchant whose premises need more than three levels cannot model its real structure, has to collapse levels or pad names, and the plan no longer matches the real premises. |
10. Release Plan & Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - FLR in the URD feature catalog |
| Rollout | F&B / dine-in merchants; no rollout toggle |
| Data setup | None - new allocation records; existing merchants gain an empty floor model |
| Launch criteria | A layout with its zone tree and units saves atomically; depth, layout-rooted, and quantity checks (floor/room/table/seat) hold; opening a dine-in order occupies units and turns them busy live; free-table queries exclude occupied units; transfer moves a party atomically; freeing a table completes its usage, and a repeated complete/cancel on an already-closed usage is silently ignored; each order keeps its allocation snapshot |
| Monitoring | Failed-save rate by reason (too deep, wrong layout, over quantity or capacity cap), delay in occupancy updates reaching Sale POS terminals, response time of free-table lookups |
References
- URD: Commerce - FLR
- Sibling PRDs: Organizer & merchant · Receipt templates · Retail business type
- Module: Commerce - URD
- Related module: Sale - orders that occupy units
- Developer: @nx/commerce · @nx/sale · @nx/core
Risks & Open Questions
| # | Risk / question | Mitigation / status |
|---|---|---|
| 01 | A half-built floor on a failed save (zones but no units) | The whole tree is one all-or-nothing save - any failure rolls back fully |
| 02 | A runaway or cyclic zone tree | Depth is capped at two levels and zones not rooted in their layout are refused before being saved |
| 03 | A floor, room, or table over its quantity cap, or a table declaring an oversized capacity | The quantity and capacity checks reject the save before anything is written |
| 04 | Two parties seated on one table (double-sell) | A unit is busy while any reserved/active/success usage exists; availability excludes occupied units |
| 05 | Floor plan drifts between terminals | Every occupancy change broadcasts a real-time update consumed by all Sale POS terminals |
| 06 | Allocation lost if usage records change | Each order keeps an allocation snapshot (the unit, its zone path, and guest details) on its own record |
| 07 | Stale-window tables never freed | A default 90-minute reservation window is recorded; completion frees the table explicitly |
Frequently asked questions
| # | Question | Answer |
|---|---|---|
| 01 | Why are the floor plan and table occupancy kept separate? | Because they change at completely different rates. The plan is a rare edit on the Client page; occupancy churns every seating on the Sale POS. Keeping them apart means the live floor never rewrites the plan, and orders always point at a stable unit. |
| 02 | How deep and how big can my floor go? | Two levels below the layout root - in practice floor → room → table - with quantity caps: at most 100 floors per layout, 50 rooms per floor, 50 tables per room, 50 tables per zone, and each table at most 50 seats. A save that would go over any of these, or that points a zone at the wrong layout, is refused before anything saves. |
| 03 | When does a table show busy? | While it holds a usage that is reserved, active, or paid (success). Once that usage is completed or cancelled, the table is free again. |
| 04 | What happens if I free an already-freed table? | Nothing - a usage already completed or cancelled silently ignores any further complete/cancel command, no error, so freeing a batch of tables at the end of the day never gets stuck on one table that was already closed. |
| 05 | What happens to the old table when I transfer a party? | The transfer is atomic: the source usages are cancelled and the target zone's units are occupied for each affected order, so the old table frees and the new one fills together. |
| 06 | Do I lose the table assignment if the live usage changes? | No - each order keeps an allocation snapshot of its unit and the unit's zone path (plus guest details), so the assignment survives on the order itself. |
| 07 | Can I find an open table of the right size? | Yes - the Sale POS queries available units in a zone (and child zones where every table is free), which you can filter by capacity. |
| 08 | Who can design the floor plan, and who can only view it? | The owner or manager designs the plan on the Client page. Staff on the Sale POS only see the live plan and can seat / find-free / transfer / free a table - they cannot create or edit the plan. |