PRD: Bố trí bàn & sơ đồ sàn
| Module | Commerce (CORE-03) | PRD ID | PRD-FLR-001 |
| Status | Shipped | Owner | Commerce squad |
| Date | 2026-06-15 | Version | v1.0 |
| Packages | @nx/commerce · @nx/sale · @nx/core · apps/sale-renderer | URD | FLR |
TL;DR
Cho một merchant F&B một sơ đồ sàn hoạt động và trạng thái chiếm bàn trực tiếp. Chủ quán thiết kế sàn một lần - một layout có tên chứa một cây zone (sàn → phòng → bàn, sâu tối đa hai cấp) với các zone lá mang unit (bàn) có sức chứa, vị trí trên canvas và style - và dựng hoặc sửa cả cây trong một thao tác aggregate nguyên tử. Tại POS, mở một đơn dine-in sẽ chiếm dụng một hoặc nhiều unit; sơ đồ sàn hiển thị mỗi unit là trống hay bận theo thời gian thực, nhân viên có thể tìm bàn trống, chuyển một nhóm khách từ vùng này sang vùng khác, và bàn được giải phóng khi usage của nó hoàn thành. Mỗi thay đổi chiếm dụng phát một sự kiện trực tiếp để mọi máy đồng bộ, và mỗi đơn hàng giữ một snapshot của unit và đường dẫn zone của nó.
1. Context & Problem
Onboarding dựng lên một tổ chức và các merchant của nó (PRD-ORG-001), nhưng một nhà hàng phục vụ tại bàn vận hành mỗi ngày dựa trên thứ mà onboarding không cung cấp: một bản đồ phòng và ai đang ngồi ở đâu. Thiếu nó, phục vụ không thể nhìn ngay những bàn nào còn trống, bếp không có điểm neo cho một đơn dine-in, và "chuyển bàn 4 ra sân vườn" là một lời dặn miệng không được ghi lại.
Hai công việc tách biệt ẩn sau "bàn". Một là thiết kế - bố trí không gian vật lý (vùng, bàn, ghế, vị trí mỗi thứ trên màn hình) - một tác vụ back-office, ít làm, thuộc về catalogue của merchant. Hai là chiếm dụng - đơn hàng đang sống nằm ở bàn nào, ngay lúc này - một mối quan tâm POS thời gian thực, tần suất cao. Mô hình hóa cả hai thành một khối có thể sửa sẽ khiến bản đồ sàn bị xáo trộn mỗi lần xếp khách, còn mô hình hóa chiếm dụng mà không có một bản đồ ổn định sẽ khiến đơn hàng trỏ vào hư không.
Increment này tách hai thứ rạch ròi: một mô hình sàn ổn định, theo merchant (layout / zone / unit) được tạo dưới dạng cây nguyên tử, và một bản ghi usage trực tiếp gắn một đơn hàng với một hoặc nhiều unit, điều khiển màu của mỗi ô trên sơ đồ sàn, và là nguồn sự thật cho tính khả dụng, chuyển bàn, tách và gộp.
2. Goals & Non-Goals
Goals
- Mô hình hóa sàn vật lý của một merchant thành đồ thị layout → cây zone → unit có tên, với unit mang sức chứa, vị trí canvas và style.
- Tạo cả sàn trong một aggregate nguyên tử (create và smart-update), với giới hạn độ sâu và một guard kiểm tra zone thuộc đúng layout.
- Chiếm dụng unit bằng cách mở một đơn dine-in; điều khiển một sơ đồ sàn trực tiếp hiển thị mỗi unit trống hay bận.
- Tìm bàn trống - các unit khả dụng trong một zone, và các zone con mà mọi bàn đều trống.
- Chuyển một nhóm khách giữa các zone, và giữ phân bổ đúng khi một đơn bị tách hoặc gộp.
- Phát một sự kiện thời gian thực trên mỗi thay đổi chiếm dụng; giữ một snapshot phân bổ trên đơn hàng.
Non-Goals
- Đặt chỗ như một sản phẩm booking (danh sách chờ, đặt cọc, lịch) - mô hình usage hỗ trợ một principal đặt chỗ, nhưng trải nghiệm booking là một increment riêng.
- Giá hay phí dịch vụ theo zone/bàn - giá nằm trong fares; phân bổ không mang tiền.
- Một UI trình soạn sơ đồ sàn đồ họa - PRD này đặc tả mô hình và các thao tác của nó; công cụ canvas là việc của
apps/sale-renderer. - KDS / điều phối bếp và bản thân vòng đời đơn hàng (Sale) - phân bổ tham chiếu một đơn, nó không sở hữu đơn.
3. Success Metrics
| Chỉ số | Mục tiêu / tín hiệu |
|---|---|
| Toàn vẹn sàn | Một bước con lỗi trong aggregate không để lại cây dựng dở - layout, zone và unit commit cùng nhau hoặc không gì cả |
| An toàn độ sâu | Không aggregate nào lưu một cây zone sâu hơn giới hạn hai cấp, hoặc một zone không thuộc layout của nó |
| Sự thật chiếm dụng | Một unit hiển thị bận đúng khi nó có một usage reserved/active/success, và trống khi ngược lại |
| Chính xác khả dụng | "Bàn trống" chỉ trả về các unit không có usage trực tiếp; một zone chỉ được đề xuất khi mọi bàn của nó đều trống |
| Đồng bộ trực tiếp | Mọi chiếm dụng / giải phóng / chuyển bàn phản ánh trên mọi máy POS mà không cần làm mới thủ công |
4. Personas & Use Cases
| Persona | Mục tiêu trong tính năng này |
|---|---|
| Chủ / Quản lý | Bố trí sàn một lần - vùng, bàn, ghế, vị trí - và sửa khi không gian thay đổi |
| Phục vụ / Thu ngân | Xem bàn nào trống, xếp một nhóm khách, chuyển nhóm, và giải phóng bàn khi khách rời |
| Lễ tân | Đọc sàn trực tiếp trong một cái liếc và chọn một bàn trống đúng kích cỡ |
Kịch bản chính: chủ quán thiết kế "Tầng Trệt" thành một layout với hai phòng, mỗi phòng chứa vài bàn, trong một lần lưu. Trong ca, phục vụ mở một đơn dine-in trên Bàn 7; ô bàn chuyển sang bận trên mọi máy. Một nhóm sáu khách vãng lai đến - phục vụ lọc bàn trống chứa được sáu, xếp họ, rồi sau đó chuyển họ từ phòng chính ra sân vườn trong một thao tác. Khi mỗi nhóm rời đi, hoàn thành usage của đơn sẽ giải phóng bàn về xanh.
5. User Stories
- Là chủ quán, tôi thiết kế sàn thành vùng và bàn trong một lần lưu, để bản đồ hoàn chỉnh và nhất quán ngay khi nó tồn tại.
- Là chủ quán, tôi sửa layout - thêm phòng, bỏ bàn, đặt lại vị trí ghế - qua một smart-update mà không dựng lại.
- Là phục vụ, tôi xem bàn trống và bận trực tiếp, để không bao giờ xếp khách vào bàn đang bận.
- Là phục vụ, tôi mở một đơn trên một bàn và nó lập tức hiện bận với mọi người, để sàn không bao giờ bị bán trùng.
- Là phục vụ, tôi chuyển một nhóm đang ngồi sang vùng khác trong một thao tác, và bàn cũ trống còn bàn mới đầy.
- Là phục vụ, tôi hoàn thành một bàn khi khách rời và nó trống trở lại, để nhóm tiếp theo được xếp.
6. Functional Requirements
| # | Yêu cầu | URD ref |
|---|---|---|
| FR-1 | Một merchant mô hình hóa sàn thành một layout có tên chứa một cây zone; zone lá sở hữu unit có sức chứa, vị trí và style | URD-FLR-001..003 |
| FR-2 | Một layout được tạo cùng cả cây zone lồng nhau trong một aggregate nguyên tử | URD-FLR-004 |
| FR-3 | Một aggregate layout cập nhật theo quy ước id: chỉ-id = xóa (cascade), id+data = cập nhật, không-id = tạo | URD-FLR-005 |
| FR-4 | Một zone tự nó cũng create/update được như một aggregate với zone con và unit lồng nhau, và zone có thể được tạo hàng loạt | URD-FLR-006 |
| FR-5 | Độ sâu bị giới hạn ở hai cấp dưới gốc layout; cây sâu hơn, hoặc một zone không thuộc layout của nó, bị từ chối trước khi lưu | URD-FLR-007 |
| FR-6 | Một aggregate layout đọc được với độ sâu tối đa cấu hình được - cây đầy đủ cho back-office, nông cho POS | URD-FLR-008 |
| FR-7 | Layout, zone và unit mang một trạng thái vòng đời (Activated / Deactivated / Archived) và được soft-delete; mỗi cái cũng quản lý độc lập | URD-FLR-009..010 |
| FR-8 | Mở một đơn dine-in trên một hoặc nhiều unit tạo một usage chiếm dụng đánh dấu các unit đó bận | URD-FLR-011 |
| FR-9 | Một usage mang một khung thời gian đặt chỗ; nếu không cho thời điểm kết thúc, một khung mặc định 90 phút áp dụng | URD-FLR-012 |
| FR-10 | Chiếm dụng theo reserved/active → success → completed (giải phóng) hoặc cancelled; một usage terminal không thể bị hủy lại | URD-FLR-013 |
| FR-11 | Một unit bận khi nó giữ một usage reserved/active/success và trống khi ngược lại; POS có thể truy vấn unit trống và zone con hoàn toàn trống | URD-FLR-014..015 |
| FR-12 | Một nhóm có thể được chuyển giữa các zone - hủy usage nguồn, chiếm dụng các unit của zone đích cho mỗi đơn, nguyên tử | URD-FLR-016 |
| FR-13 | Tách một đơn nhân bản các usage đang hoạt động của nó sang mỗi đơn mới; gộp chuyển chúng sang đơn còn sống | URD-FLR-017 |
| FR-14 | Mỗi thay đổi chiếm dụng phát một sự kiện sơ đồ sàn thời gian thực tới mọi máy | URD-FLR-018 |
| FR-15 | Một usage có thể ghi thông tin khách, và đơn/đặt chỗ giữ một snapshot phân bổ (unit + đường dẫn zone + khách) | URD-FLR-019..020 |
| FR-16 | Mọi thao tác phân bổ scope theo merchant (x-merchant-id) và được gác bởi quyền phân bổ | URD-FLR-021 |
Toàn văn yêu cầu và tiêu chí chấp nhận nằm trong Commerce URD - FLR. PRD này tham chiếu thay vì lặp lại.
7. Non-Functional Requirements
| Lĩnh vực | Yêu cầu |
|---|---|
| Atomicity | Mô hình sàn (layout + zone + unit) và mỗi thao tác chiếm dụng (start, transfer, split, merge) đều là transaction tất-cả-hoặc-không |
| Depth safety | Một guard cây đệ quy giới hạn độ sâu zone và từ chối tham chiếu zone không thuộc layout trước khi ghi gì |
| Real-time | Thay đổi chiếm dụng đẩy sự kiện trực tiếp để mọi máy POS phản ánh sàn mà không poll |
| Tenancy & authz | Mọi thao tác scope theo merchant (x-merchant-id); thiết kế sàn gác bởi quyền allocation-layout/zone/unit, chiếm dụng bởi quyền allocation-usage |
| Performance | Khả dụng giải các unit của một zone qua một truy vấn đệ quy đơn; đọc sàn giới hạn độ sâu nên POS chỉ lấy phần nó render |
| Durability | Mỗi đơn giữ một snapshot phân bổ (unit + đường dẫn zone + khách) để phân bổ tồn tại trên tài liệu độc lập với các dòng usage trực tiếp |
| i18n | Tên layout, zone và unit song ngữ ({ en, vi }) |
8. UX & Flows
Vòng đời chiếm dụng
Xếp khách, xem trực tiếp, chuyển bàn
Mô hình sàn hiện ở back-office như một trình soạn layout (vùng, bàn, sức chứa, vị trí) và ở POS (apps/sale-renderer) như sơ đồ sàn trực tiếp tô màu mỗi ô theo chiếm dụng và điều khiển xếp / tìm-trống / chuyển / hoàn thành.
9. Data & Domain
| Thực thể | Vai trò |
|---|---|
AllocationLayout | Bản đồ sàn có tên của một merchant; gốc của cây zone; mang style canvas |
AllocationZone | Một vùng trong cây (nhóm sàn / phòng / bàn); tự lồng tối đa hai cấp dưới layout |
AllocationUnit | Một đơn vị có thể ngồi (bàn); mang sức chứa, vị trí canvas và style; thuộc một zone lá |
AllocationUsage | Một chiếm dụng trực tiếp gắn một unit với một đơn hàng (hoặc đặt chỗ), với một khung đặt chỗ và một trạng thái điều khiển màu sàn |
Chỉ mang tính khái niệm - schema đầy đủ và bất biến nằm trong commerce domain model. Quan hệ là tham chiếu mềm; toàn vẹn được thực thi trong service aggregate và usage, không phải bởi ràng buộc cơ sở dữ liệu.
10. Dependencies & Assumptions
Phụ thuộc vào
- Merchant (MER, PRD-ORG-001) - mọi layout, zone, unit và usage scope theo một merchant.
- Đơn hàng (Sale) - một usage chiếm unit thay cho một đơn dine-in; tách / gộp / thanh toán điều khiển vòng đời của nó.
@nx/core- các model thực thể phân bổ, trạng thái, và kênh socket trực tiếp.
Giả định
- Loại hình kinh doanh của merchant là phục vụ tại bàn (F&B); merchant counter / bán lẻ không cần mô hình sàn.
- Độ sâu zone thực tế nhiều nhất là sàn → phòng → bàn; giới hạn hai cấp phản ánh điều đó.
- Một đơn hàng là principal chiếm dụng thông thường; principal đặt chỗ được mô hình hỗ trợ nhưng UX booking của nó ngoài phạm vi ở đây.
11. Risks & Open Questions
| Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|
| Một sàn dựng dở trên aggregate lỗi (có zone nhưng không unit) | Cả cây là một transaction - bất kỳ lỗi nào cũng rollback hoàn toàn |
| Một cây zone chạy loạn hoặc lặp vòng | Độ sâu giới hạn hai cấp và tham chiếu không thuộc layout bị từ chối trước khi lưu |
| Hai nhóm xếp vào một bàn (bán trùng) | Một unit bận khi tồn tại bất kỳ usage reserved/active/success; khả dụng loại trừ unit đang bận |
| Sơ đồ sàn lệch giữa các máy | Mỗi thay đổi chiếm dụng phát một sự kiện trực tiếp được mọi máy POS tiêu thụ |
| Mất phân bổ nếu các dòng usage thay đổi | Mỗi đơn giữ một snapshot phân bổ (unit + đường dẫn zone + khách) trên tài liệu của riêng nó |
| Bàn hết khung giờ không bao giờ được giải phóng | Một khung đặt chỗ mặc định 90 phút được ghi; hoàn thành giải phóng bàn một cách tường minh |
12. Release Plan & Launch Criteria
| Khía cạnh | Kế hoạch |
|---|---|
| Phase | P2 - FLR trong URD feature catalog |
| Rollout | Merchant F&B / dine-in; không feature flag |
| Migration | Không - thực thể phân bổ mới; merchant hiện hữu nhận một mô hình sàn rỗng |
| Launch criteria | Một layout với cây zone và unit lưu nguyên tử; guard độ sâu và thuộc-layout giữ vững; mở một đơn dine-in chiếm unit và chuyển chúng bận trực tiếp; truy vấn bàn trống loại trừ unit đang bận; chuyển bàn di chuyển một nhóm nguyên tử; hoàn thành giải phóng bàn; mỗi đơn giữ snapshot phân bổ |
| Monitoring | Tỷ lệ lỗi aggregate theo lý do (độ sâu, không thuộc layout), độ trễ giao sự kiện chiếm dụng, độ trễ truy vấn khả dụng |
13. FAQ
Tại sao bản đồ sàn và chiếm bàn tách biệt? Vì chúng thay đổi với tần suất hoàn toàn khác nhau. Bản đồ là một chỉnh sửa back-office hiếm hoi; chiếm dụng xáo trộn mỗi lần xếp khách. Giữ chúng tách biệt nghĩa là sàn trực tiếp không bao giờ ghi đè bản đồ, và đơn hàng luôn trỏ vào một unit ổn định.
Sàn của tôi sâu được bao nhiêu? Hai cấp dưới gốc layout - thực tế là sàn → phòng → bàn. Một aggregate sâu hơn, hoặc trỏ một zone vào sai layout, bị từ chối trước khi lưu gì.
Khi nào một bàn hiện bận? Khi nó giữ một usage là reserved, active, hoặc đã trả (success). Khi usage đó được hoàn thành hoặc hủy, bàn trống trở lại.
Chuyện gì xảy ra với bàn cũ khi tôi chuyển một nhóm? Việc chuyển là nguyên tử: usage nguồn bị hủy và các unit của zone đích được chiếm cho mỗi đơn liên quan, nên bàn cũ trống và bàn mới đầy cùng nhau.
Tôi có mất phân bổ bàn nếu usage trực tiếp thay đổi không? Không - mỗi đơn giữ một snapshot phân bổ của unit và đường dẫn zone của unit (cùng thông tin khách), nên phân bổ tồn tại trên chính đơn hàng.
Tôi có thể tìm một bàn trống đúng kích cỡ không? Có - POS truy vấn unit khả dụng trong một zone (và zone con mà mọi bàn đều trống), bạn có thể lọc theo sức chứa.
References
- URD: Commerce - FLR
- PRD liên quan: Tổ chức & merchant · Mẫu biên lai · Loại hình bán lẻ
- Module: Commerce - tổng quan + năng lực
- Module liên quan: Sale - đơn hàng chiếm dụng unit
- Developer: @nx/commerce · @nx/sale · @nx/core