Skip to content

PRD: Table & floor allocation

ModuleCommerce (CORE-03)PRD IDPRD-FLR-001
StatusShippedOwnerCommerce squad
Date2026-06-15Versionv1.0
Packages@nx/commerce · @nx/sale · @nx/core · apps/sale-rendererURDFLR

TL;DR

Gives an F&B merchant a working floor plan and live table occupancy. The owner designs the floor once - a named layout holding a tree of zones (floor → room → table, up to two levels deep) whose leaf zones carry units (tables) with a capacity, a position on the canvas, and a style - and builds or edits the whole tree in one atomic aggregate operation. At the POS, opening a dine-in order occupies one or more units; the floor plan shows each unit as free or busy in real time, staff can find free tables, transfer a party from one zone to another, and the table is freed when its usage is completed. Every occupancy change broadcasts a live event so all terminals stay in sync, and each order keeps a snapshot of the unit and its zone path.

1. Context & Problem

Onboarding stands up an organization and its merchants (PRD-ORG-001), but a sit-down restaurant runs its day on something onboarding does not provide: a map of the room and who is sitting where. Without it, 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.

Two distinct jobs hide behind "tables". One is design - laying out the physical space (zones, tables, seats, where each sits on the screen) - a back-office, infrequent task that belongs to the merchant's catalogue. The other is occupancy - which table a live order is on, right now - a high-frequency, real-time POS concern. Modelling them as one editable blob would make the floor map churn on every seating, and modelling occupancy without a stable map would leave orders pointing at nothing.

This increment separates the two cleanly: a stable, merchant-scoped floor model (layout / zone / unit) authored as an atomic tree, and a live usage record that binds a sale order to one or more units, drives the colour of every tile on the floor plan, and is the source of truth for availability, transfer, split, and merge.

2. Goals & Non-Goals

Goals

  • Model a merchant's physical floor as a named layout → zone tree → unit graph, with units carrying capacity, canvas placement, and style.
  • Author the whole floor in one atomic aggregate (create and smart-update), with a depth cap and a layout-rooted guard.
  • Occupy units by opening a dine-in sale order; 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 event on every occupancy change; keep an allocation snapshot on the order.

Non-Goals

  • Reservations as a booking product (wait-lists, deposits, calendar) - the usage model supports a reservation principal, but the booking experience is a separate increment.
  • Pricing or service charge per zone/table - prices live in fares; allocation carries no money.
  • A graphical floor-plan editor UI - this PRD specifies the model and its operations; the canvas tooling is an apps/sale-renderer concern.
  • KDS / kitchen routing and the order lifecycle itself (Sale) - allocation references an order, it does not own it.

3. Success Metrics

MetricTarget / signal
Floor integrityA failed sub-step in an aggregate leaves no half-built tree - layout, zones, and units commit together or not at all
Depth safetyNo aggregate ever persists a zone tree deeper than the two-level cap, or a zone not rooted in its layout
Occupancy truthA 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 syncEvery occupy / free / transfer reflects on all POS terminals without a manual refresh

4. Personas & Use Cases

PersonaGoal in this feature
Owner / ManagerLay out the floor once - zones, tables, seats, positions - and edit it as the space changes
Server / CashierSee which tables are free, seat a party, move it, and free the table when guests leave
HostRead the live floor at a glance and pick an open table of the right size

Core scenario: an owner designs "Ground Floor" as a layout with two rooms, each holding several tables, in one save. During service a server 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.

5. User Stories

  • As an owner, I design my floor as zones and tables in one save, so the map is complete and consistent the moment it exists.
  • As an owner, I edit the layout - add a room, remove a table, reposition seats - through one smart-update without rebuilding it.
  • As a server, I see free vs busy tables live, so I never seat a party on an occupied table.
  • As a server, I open an order on a table and it immediately shows busy to everyone, so the floor is never double-sold.
  • As a server, I transfer a seated party to another zone in one action, and the old table frees while the new one fills.
  • As a server, I complete a table when guests leave and it turns free again, so the next party can be seated.

6. Functional Requirements

#RequirementURD ref
FR-1A merchant models its floor as a named layout holding a tree of zones; leaf zones own units with capacity, placement, and styleURD-FLR-001..003
FR-2A layout is created with its whole nested zone tree in one atomic aggregateURD-FLR-004
FR-3A layout aggregate updates by the id-convention: id-only = delete (cascades), id+data = update, no id = createURD-FLR-005
FR-4A zone is itself create/update-able as an aggregate with nested sub-zones and units, and zones can be batch-createdURD-FLR-006
FR-5Depth is capped at two levels below the layout root; a deeper tree, or a zone not rooted in its layout, is refused before persistenceURD-FLR-007
FR-6A layout aggregate is readable with a configurable max depth - full tree for back-office, shallow for the POSURD-FLR-008
FR-7Layouts, zones, and units carry a lifecycle status (Activated / Deactivated / Archived) and are soft-deleted; each is also managed standaloneURD-FLR-009..010
FR-8Opening a dine-in sale order on one or more units creates an occupancy usage that marks those units busyURD-FLR-011
FR-9A usage carries a reservation window; with no end given, a default 90-minute window appliesURD-FLR-012
FR-10Occupancy follows reserved/active → success → completed (freed) or cancelled; a terminal usage cannot be cancelled againURD-FLR-013
FR-11A unit is busy while it holds a reserved/active/success usage and free otherwise; the POS can query free units and fully-free child zonesURD-FLR-014..015
FR-12A party can be transferred between zones - source usages cancelled, the target zone's units occupied per order, atomicallyURD-FLR-016
FR-13Splitting an order clones its active usages onto each new order; merging moves them to the surviving orderURD-FLR-017
FR-14Every occupancy change broadcasts a real-time floor-plan event to all terminalsURD-FLR-018
FR-15A usage may capture guest details, and the order/reservation keeps an allocation snapshot (unit + zone path + guest)URD-FLR-019..020
FR-16All allocation operations are scoped per merchant (x-merchant-id) and gated by allocation permissionsURD-FLR-021

Full requirement text and acceptance criteria live in the Commerce URD - FLR. This PRD references them rather than restating them.

7. Non-Functional Requirements

AreaRequirement
AtomicityThe floor model (layout + zones + units) and each occupancy operation (start, transfer, split, merge) are all-or-nothing transactions
Depth safetyA recursive-tree guard caps zone depth and rejects non-layout-rooted references before anything is written
Real-timeOccupancy changes push live events so every POS terminal reflects the floor without polling
Tenancy & authzAll operations scoped per merchant (x-merchant-id); floor design gated by allocation-layout/zone/unit permissions, occupancy by allocation-usage permissions
PerformanceAvailability resolves a zone's units through a single recursive query; the floor read caps depth so the POS fetches only what it renders
DurabilityEach order keeps an allocation snapshot (unit + zone path + guest) so the assignment survives on the document independent of the live usage rows
i18nLayout, zone, and unit names are bilingual ({ en, vi })

8. UX & Flows

Occupancy lifecycle

Seat, see live, transfer

The floor model surfaces in back-office as a layout editor (zones, tables, capacities, positions) and in the POS (apps/sale-renderer) as the live floor plan that colours each tile by occupancy and powers seat / find-free / transfer / complete.

9. Data & Domain

EntityRole
AllocationLayoutA merchant's named floor map; root of the zone tree; carries canvas style
AllocationZoneA region in the tree (floor / room / table grouping); self-nesting up to two levels under the layout
AllocationUnitA seatable unit (table); carries capacity, canvas placement, and style; belongs to a leaf zone
AllocationUsageA live occupancy binding a unit to a sale order (or reservation), with a reservation window and a status that drives the floor colour

Conceptual only - the full schema and invariants live in the commerce domain model. Relations are soft references; integrity is enforced in the aggregate and usage services, not by database constraints.

10. Dependencies & Assumptions

Depends on

  • Merchant (MER, PRD-ORG-001) - every layout, zone, unit, and usage is scoped to a merchant.
  • Sale orders (Sale) - a usage occupies units on behalf of a dine-in order; split / merge / payment drive its lifecycle.
  • @nx/core - the allocation entity models, statuses, and the live socket channel.

Assumptions

  • The merchant's business type is dine-in (F&B); counter / retail merchants do not need a floor model.
  • Zone depth in practice is at most floor → room → table; the two-level cap reflects that.
  • An order is the usual occupancy principal; the reservation principal is supported by the model but its booking UX is out of scope here.

11. Risks & Open Questions

Risk / questionMitigation / status
A half-built floor on a failed aggregate (zones but no units)The whole tree is one transaction - any failure rolls back fully
A runaway or cyclic zone treeDepth is capped at two levels and non-layout-rooted references are refused before persistence
Two parties seated on one table (double-sell)A unit is busy while any reserved/active/success usage exists; availability excludes occupied units
Floor plan drifts between terminalsEvery occupancy change broadcasts a live event consumed by all POS terminals
Allocation lost if usage rows changeEach order keeps an allocation snapshot (unit + zone path + guest) on its own document
Stale-window tables never freedA default 90-minute reservation window is recorded; completion frees the table explicitly

12. Release Plan & Launch Criteria

AspectPlan
PhaseP2 - FLR in the URD feature catalog
RolloutF&B / dine-in merchants; no feature flag
MigrationNone - new allocation entities; existing merchants gain an empty floor model
Launch criteriaA layout with its zone tree and units saves atomically; depth and layout-rooted guards hold; opening a dine-in order occupies units and turns them busy live; free-table queries exclude occupied units; transfer moves a party atomically; completing frees the table; each order keeps its allocation snapshot
MonitoringAggregate failure rate by reason (depth, not-rooted), occupancy event-delivery lag, available-query latency

13. FAQ

Why are the floor map and table occupancy separate? Because they change at completely different rates. The map is a rare back-office edit; occupancy churns every seating. Keeping them apart means the live floor never rewrites the map, and orders always point at a stable unit.

How deep can my floor go? Two levels below the layout root - in practice floor → room → table. An aggregate that would go deeper, or that points a zone at the wrong layout, is refused before anything saves.

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.

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.

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.

Can I find an open table of the right size? Yes - the POS queries available units in a zone (and child zones where every table is free), which you can filter by capacity.

References

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.