PRD: Tổ chức thực đơn & hiển thị theo kênh
| Module | Sản phẩm | PRD ID | PRD-SCH-001 |
| Trạng thái | Sẵn sàng dev | FEAT | SCH |
| Epic | — | Plane | BANA-1552 |
| Ngày | 2026-06-15 | Phiên bản | v1.0 |
| Gói | @nx/commerce · @nx/core · @nx/search | URD | SCH |
| Surface | Client · Chủ/QLSale · POS | ||
| Phụ trách | Phát Nguyễn | ||
Tổ chức thực đơn & hiển thị theo kênh là gì
Danh mục (PRD-PRD-001) cho biết merchant bán cái gì, nhưng không cho biết mỗi sản phẩm được chào bán ở đâu. Đợt này sắp xếp những gì merchant bán thành các kênh bán hàng (Channel) - bề mặt thực đơn có tên, đa ngôn ngữ, phân cấp (tại quầy, mang đi, giao hàng, ki-ốt) - và quyết định sản phẩm nào hiển thị trong từng kênh. Gán sản phẩm vào kênh chính là công tắc bật/tắt hiển thị; mọi danh sách kênh và sản phẩm đều được trả về sau khi lọc theo quyền của người gọi, nên một thiết bị chỉ thấy đúng thực đơn merchant của mình.
Hai khoảng trống mà kênh giải quyết
Một merchant hiếm khi bán cùng một danh sách ở mọi nơi: thực đơn tại chỗ khác với giao hàng, ki-ốt chỉ bày một phần, quầy thời vụ cần bề mặt riêng. Thiếu khái niệm kênh, mọi thiết bị đều thấy chung một danh mục phẳng, và merchant không thể bật một món cho giao hàng mà vẫn tắt nó tại quầy.
Kéo theo hai khoảng trống. Một kênh phải biết lấy hàng từ kho nào, kèm một mặc định hợp lý khi có nhiều vị trí phù hợp. Và thực đơn gửi tới thiết bị phải được giới hạn theo người đang hỏi - chủ hai merchant thấy kênh của đúng hai merchant đó, nhân viên chỉ thấy merchant được phân công, admin thấy tất cả - mà không ai phải tự lọc bằng tay.
Một chuỗi quán, hai kênh
Một chủ quán cà phê tạo kênh Giao hàng và kênh Tại quầy. Sản phẩm mới chỉ gán cho Tại quầy; một phần cũng gán cho Giao hàng, nên ứng dụng tài xế hiện một thực đơn gọn hơn. Chuỗi kho của Giao hàng đặt kho hậu cần lên đầu - nhưng hôm nay đơn của kênh này lấy tồn từ vị trí kho ghi trực tiếp trên kênh, không phải từ đầu chuỗi ưu tiên vừa lưu; nếu chưa ghi vị trí trực tiếp thì lùi về mặc định của merchant. Khi chủ định ngừng Tại quầy lúc một bàn vẫn còn đơn mở, hệ thống từ chối cho tới khi đơn đó đóng.
Chỗ dễ sai nhất
Chuỗi vị trí kho theo thứ tự ưu tiên chưa được đọc khi chạy
Một kênh ghi được cả một chuỗi vị trí kho theo thứ tự ưu tiên - và điều đó dễ khiến người đọc tưởng hệ thống dò qua chuỗi này để tìm nguồn hàng. Không phải vậy: hôm nay việc xuất hàng chỉ nhìn vào một vị trí kho ghi trực tiếp trên kênh; nếu trống thì lùi thẳng về vị trí mặc định của merchant. Chuỗi ưu tiên đã lưu và khớp lại đúng, nhưng chưa hề được đọc lúc xuất hàng.
Đừng đọc FR-009 (🔶) như "logic dò kho gần xong" - phần đã xong là lưu trữ, phần chưa làm là chính cái vòng dò tìm.
1. Mục tiêu & Loại trừ
Mục tiêu
- Cho chủ tạo kênh bán hàng trong merchant - tên/mô tả đa ngôn ngữ, một định danh hệ thống, một slug duy nhất trong phạm vi merchant.
- Biến việc gán sản phẩm vào kênh thành công cụ kiểm soát hiển thị: sản phẩm chỉ xuất hiện trong kênh khi được gán vào đó.
- Gán sản phẩm vào kênh ngay trong bước tạo sản phẩm toàn vẹn, để sản phẩm hiển thị ngay từ ngày đầu.
- Cho mỗi kênh một trạng thái vòng đời (kích hoạt / ngừng / lưu trữ) và từ chối ngừng khi còn đơn đang hoạt động.
- Ghi một chuỗi vị trí kho theo thứ tự ưu tiên cho mỗi kênh (ưu tiên thấp nhất = mặc định dự kiến). Lưu ý: mới chỉ lưu và khớp lại, chưa dùng khi xuất hàng - tồn hiện lấy từ vị trí kho ghi trên kênh, nếu không có thì lùi về mặc định của merchant.
- Hỗ trợ phân cấp cha-con giữa các kênh để gom nhóm bề mặt thực đơn. Lưu ý: liên kết cha đã có và đã lộ ra ngoài, nhưng chưa có chốt chặn chu trình / tự-làm-cha / xuyên-cửa-hàng (khác với nhóm hàng).
- Trả mọi danh sách kênh và sản phẩm sau khi lọc theo quyền của người gọi; admin bỏ qua bộ lọc.
Loại trừ
- Định nghĩa sản phẩm, biến thể, định danh - xem PRD-PRD-001.
- Bậc giá theo từng kênh - do fare theo ngữ cảnh kênh đảm nhận (FAR).
- Mức tồn và biến động kho - thuộc Kho; kênh chỉ tham chiếu vị trí, không giữ tồn.
- Lần lượt dò qua chuỗi kho như một fallback nhiều bước khi xuất hàng - hiện chuỗi ưu tiên hoàn toàn không được đọc khi chạy; tồn lấy từ vị trí kho ghi trên kênh, nếu không có thì lùi về mặc định của merchant, còn chuỗi theo thứ tự được giữ lại để dùng sau.
- Xử lý đơn và thanh toán qua kênh - thuộc Đơn hàng.
2. Thước đo thành công
| Chỉ số | Mục tiêu / tín hiệu |
|---|---|
| Kiểm soát hiển thị | Sản phẩm chỉ hiển thị trong kênh khi được gán; bỏ gán thì biến mất |
| Phục vụ đã giới hạn | Danh sách kênh/sản phẩm của một người chỉ trả về merchant của họ; admin thấy tất cả |
| An toàn khi ngừng | Kênh còn đơn đang hoạt động thì không ngừng được |
| Rõ nguồn kho | Mỗi kênh xác định được đúng một vị trí kho mặc định, không mơ hồ (hiện lấy từ vị trí ghi trên kênh + mặc định merchant, chưa phải từ chuỗi ưu tiên) |
| Toàn vẹn slug | Không có hai kênh chưa xóa nào trong một merchant trùng slug, bất kể trạng thái vòng đời |
3. Persona & Tình huống
| Persona | Mục tiêu trong tính năng |
|---|---|
| Chủ / Quản lý | Dựng thực đơn cho từng bề mặt, bật/tắt sản phẩm theo kênh, đặt nguồn kho cho kênh |
| Thu ngân / Thiết bị | Nhận thực đơn đã giới hạn theo merchant và kênh hiện tại |
| Admin / Super Admin | Xem kênh của mọi merchant mà không bị lọc theo vai trò |
Tình huống cốt lõi: một chủ quán cà phê tạo kênh Giao hàng và kênh Tại quầy. Sản phẩm mới chỉ gán cho Tại quầy; một phần cũng gán cho Giao hàng, nên ứng dụng tài xế hiện một thực đơn gọn hơn. Chuỗi kho của Giao hàng đặt kho hậu cần lên đầu, nên đơn của nó tự lấy hàng từ đó. Khi chủ định ngừng Tại quầy lúc một bàn vẫn còn đơn mở, hệ thống từ chối cho tới khi đơn đó đóng.
4. User Stories
| # | Là một | Tôi muốn | Để |
|---|---|---|---|
| 01 | chủ | tạo một kênh bán hàng cho mỗi bề mặt bán | mỗi thiết bị hiện đúng thực đơn |
| 02 | chủ | gán sản phẩm vào những kênh nó nên xuất hiện | hiển thị chỉ là một công tắc |
| 03 | chủ | đặt chuỗi vị trí kho cho kênh | đơn của nó tự lấy hàng đúng chỗ |
| 04 | chủ | không thể lỡ tay ngừng một kênh còn đơn đang chạy | không bỏ mắc phiếu mở nào |
| 05 | nhân viên | chỉ thấy kênh và sản phẩm của merchant được phân công | thực đơn của tôi không lẫn sang merchant khác |
| 06 | admin | đọc được kênh của mọi merchant không qua lọc vai trò | hỗ trợ bất kỳ tenant nào |
5. Yêu cầu chức năng
| # | Yêu cầu | Trạng thái | URD ref |
|---|---|---|---|
FR-001 | Chủ có thể tạo kênh bán hàng trong một merchant, mỗi kênh có tên/mô tả đa ngôn ngữ và một định danh hệ thống tự sinh. | ✅ | URD-SCH-001 |
FR-002 | Slug kênh là duy nhất trong phạm vi một merchant. • Ràng buộc hiện tính trên mọi kênh chưa xóa, không phân biệt trạng thái vòng đời. • Một kênh đã Ngừng hoặc Lưu trữ (nhưng chưa xóa) vẫn giữ chỗ slug của nó, không phải chỉ riêng các kênh đang Kích hoạt. | 🔶 | URD-SCH-002 |
FR-003 | Một kênh có trạng thái vòng đời: kích hoạt, ngừng, lưu trữ. | ✅ | URD-SCH-003 |
FR-004 | Chủ có thể cập nhật một kênh. Cập nhật rỗng (không có trường nào) bị từ chối. | ✅ | URD-SCH-004 |
FR-005 | Việc ngừng một kênh bị từ chối khi nó còn đơn bán đang hoạt động. | ✅ | URD-SCH-005 |
FR-006 | Kênh hỗ trợ phân cấp cha-con. • Liên kết cha đã có và đã lộ ra ngoài. • Hiện chưa có chốt chặn chu trình, tự-làm-cha, hay liên kết xuyên merchant - khác với nhóm hàng. | 🔶 | URD-SCH-006 |
FR-007 | Một sản phẩm được gán vào một hoặc nhiều kênh. Việc gán quyết định nó có hiển thị trong kênh đó hay không. | ✅ | URD-SCH-007 · URD-PRD-011 |
FR-008 | Các gán sản phẩm-vào-kênh được ghi như một phần của bước tạo sản phẩm toàn vẹn. | ✅ | URD-SCH-008 |
FR-009 | Một kênh ghi một chuỗi vị trí kho theo thứ tự ưu tiên (ưu tiên thấp nhất = mặc định dự kiến). • Chuỗi được khớp lại khi cập nhật, mỗi vị trí xuất hiện tối đa một lần. • Chuỗi này hiện chưa được đọc khi xuất hàng. • Đơn hàng lấy tồn từ một vị trí kho ghi trực tiếp trên kênh, nếu không có thì lùi về vị trí mặc định của merchant. | 🔶 | URD-SCH-009 |
FR-010 | Xóa một kênh sẽ bỏ gán các sản phẩm của nó và xóa kênh (có thể hoàn tác). • Các liên kết của chuỗi nguồn kho không được dọn theo. • Kênh con của kênh bị xóa không được xử lý. | 🔶 | URD-SCH-010 |
FR-011 | Danh sách/đếm kênh và sản phẩm được trả về sau khi lọc theo quyền của người gọi. Admin bỏ qua bộ lọc. | ✅ | URD-SCH-011 · URD-ACC-001..004 |
FR-012 | Kênh và sản phẩm có thể tìm kiếm, giới hạn theo các merchant của người gọi. | ✅ | URD-SCH-012 |
FR-013 | Tạo kênh đầu tiên của một merchant hoàn tất bước onboarding kênh của merchant. | ✅ | URD-SCH-013 |
5.1 Tiêu chí nghiệm thu
- Tạo một kênh với tên/mô tả song ngữ, không nhập slug → kênh được tạo với slug và một định danh hệ thống tự sinh.
- Tạo kênh A với slug "delivery", chuyển kênh A sang Ngừng, rồi tạo kênh B cũng đặt slug "delivery" trong cùng merchant → bị từ chối, vì kênh A tuy đã ngừng nhưng chưa bị xóa nên vẫn giữ chỗ slug đó; xóa hẳn kênh A rồi tạo lại kênh B với cùng slug → tạo thành công.
- Tạo một kênh mới → trạng thái mặc định là Kích hoạt; chuyển kênh đó sang Ngừng rồi sang Lưu trữ → mỗi bước chuyển đúng vòng đời.
- Gửi yêu cầu cập nhật một kênh không kèm trường nào → bị từ chối.
- Một kênh còn một đơn đang xử lý, thử chuyển kênh đó sang Ngừng → bị từ chối; đóng hoặc hủy đơn đó rồi thử lại → chuyển Ngừng thành công.
- Tạo một kênh con dưới một kênh cha đã có → liên kết cha-con được lưu và đọc lại đúng; đặt cha của một kênh trỏ ngược về chính nó, hoặc tạo thành một vòng cha-con khép kín giữa nhiều kênh → hệ thống hiện không chặn, thao tác vẫn được lưu.
- Gán một sản phẩm vào kênh Giao hàng → sản phẩm hiển thị trong kênh Giao hàng; bỏ gán → sản phẩm biến mất khỏi kênh đó, các kênh khác của nó không đổi.
- Tạo một sản phẩm mới kèm gán vào hai kênh trong cùng một lần gọi → sản phẩm và cả hai gán kênh được ghi trong một thao tác; một lỗi giữa chừng làm rollback toàn bộ, không sản phẩm hay gán kênh nào bị lưu dở.
- Đặt chuỗi vị trí kho cho một kênh theo thứ tự [Kho A, Kho B] → hai dòng được ghi đúng thứ tự ưu tiên; cập nhật lại chuỗi thành [Kho B, Kho C] → Kho A bị gỡ, Kho B giữ nguyên dòng nhưng đổi ưu tiên, Kho C được thêm mới. Tạo một đơn bán qua kênh đó → tồn được lấy từ vị trí kho ghi trực tiếp trên kênh, không phải từ đầu chuỗi ưu tiên vừa lưu; một kênh chưa có vị trí ghi trực tiếp → tồn lùi về vị trí mặc định của merchant.
- Xóa một kênh đang có sản phẩm gán và có chuỗi vị trí kho → các gán sản phẩm bị gỡ, kênh bị xóa mềm (hoàn tác được); các dòng trong chuỗi vị trí kho của kênh đó vẫn còn nguyên, không bị dọn theo, và các kênh con của kênh vừa xóa vẫn giữ nguyên liên kết cha cũ.
- Chủ merchant X đăng nhập và liệt kê kênh → chỉ kênh của merchant X được trả về; admin liệt kê → kênh của mọi merchant được trả về.
- Tìm kiếm kênh hoặc sản phẩm theo từ khóa bằng tài khoản chủ merchant Y → kết quả giới hạn trong các merchant của Y.
- Một merchant chưa từng tạo kênh nào → bước onboarding "Kênh bán hàng" hiện chưa hoàn tất; tạo kênh đầu tiên của merchant đó → bước onboarding đó hiện hoàn tất ngay khi đọc lại.
6. Yêu cầu phi chức năng
| Lĩnh vực | Yêu cầu |
|---|---|
| Toàn vẹn | Tạo kênh cùng chuỗi kho của nó là một thao tác; tạo sản phẩm ghi các gán kênh trong cùng bước toàn vẹn - lỗi giữa chừng thì roll back trọn vẹn |
| Tenancy & truy cập | Mọi thao tác giới hạn trong merchant của người dùng và được kiểm soát bằng quyền kênh bán hàng |
| Đọc đã giới hạn theo quyền | Danh sách, đếm và tìm kiếm xác định các quyền merchant của người gọi rồi lọc về chúng; người gọi admin bỏ qua |
| Duy nhất | Slug kênh duy nhất theo merchant giữa các hàng chưa xóa, không phân biệt trạng thái vòng đời; một vị trí kho xuất hiện tối đa một lần trong một kênh |
| Xóa hoàn tác được | Kênh và các gán bị xóa đều có thể hoàn tác; không có gì bị xóa vật lý |
| Ngôn ngữ | Tên và mô tả kênh là song ngữ (tiếng Anh và tiếng Việt) |
7. UX & Luồng
Bề mặt kênh cho phép chủ đặt tên kênh, đặt slug, sắp xếp chuỗi vị trí kho (trên cùng = nguồn mặc định) và đặt nó dưới một kênh cha. Bề mặt sản phẩm cho phép chủ chọn các kênh mà một sản phẩm sẽ xuất hiện. Thiết bị yêu cầu thực đơn và chỉ nhận về các kênh và sản phẩm mà quyền của họ cho phép.
8. Dữ liệu & Miền nghiệp vụ
| Khái niệm | Vai trò |
|---|---|
| Kênh bán hàng | Một bề mặt thực đơn trong một merchant - tên, slug, trạng thái, cha tùy chọn |
| Liên kết kênh-sản phẩm | Liên kết hiển thị - sản phẩm chỉ hiện trong kênh khi liên kết này còn sống |
| Vị trí kho của kênh | Một vị trí trong chuỗi nguồn kho theo thứ tự ưu tiên của kênh (ưu tiên thấp nhất = mặc định dự kiến); đã ghi nhưng chưa được đọc khi xuất hàng |
Chỉ ở mức khái niệm - chi tiết đầy đủ và bất biến nằm trong mô hình miền commerce. Liên kết là tham chiếu mềm; toàn vẹn được thực thi trong logic kênh và sản phẩm, không phải bằng khóa ngoại CSDL.
9. Phụ thuộc & Giả định
Phụ thuộc vào
| # | Feature | Phụ thuộc điều gì |
|---|---|---|
| 01 | Danh mục sản phẩm (PRD-PRD-001, PRD) | Sản phẩm là thứ kênh làm hiển thị. |
| 02 | Merchant (Commerce) | Kênh giới hạn trong một merchant; kênh đầu tiên hoàn tất một bước onboarding merchant. |
| 03 | Kho (Kho) | Chuỗi tham chiếu các vị trí kho; một vị trí phải tồn tại trước khi được thêm. |
| 04 | Đơn hàng (Đơn hàng) | Chốt chặn khi ngừng đọc các đơn đang hoạt động của một kênh. |
| 05 | Tìm kiếm | Kênh và sản phẩm được lập chỉ mục và phục vụ đã giới hạn theo các merchant của người gọi. |
Giả định
| # | Giả định | Sai thì sao |
|---|---|---|
| 01 | Một merchant đã tồn tại và quyền của người gọi xác định được những merchant họ được thấy. | Thiếu quyền hoặc merchant không tồn tại thì thao tác tạo kênh hoặc đọc danh sách kênh bị chặn ngay từ bước kiểm tra, không tới được tầng đọc/ghi dữ liệu. |
| 02 | Các vị trí kho mà chuỗi của một kênh tham chiếu thuộc cùng merchant. | Một vị trí kho thuộc merchant khác lọt vào chuỗi thì kênh có thể trỏ nguồn hàng sang dữ liệu không thuộc về mình. |
10. Kế hoạch phát hành & Tiêu chí
| Khía cạnh | Kế hoạch |
|---|---|
| Phase | P2 - SCH trong danh mục tính năng URD |
| Triển khai | Mọi merchant; không feature flag |
| Migration | Không - kênh và các gán chạy trên nền tảng danh mục hiện có |
| Tiêu chí ra mắt | Sản phẩm chỉ hiển thị trong kênh khi được gán; tạo kênh ghi lại chuỗi vị trí kho của nó (mặc định khi chạy lấy từ vị trí ghi trên kênh + mặc định merchant); ngừng khi còn đơn đang hoạt động bị từ chối; mọi danh sách kênh/sản phẩm được lọc về các merchant của người gọi; admin bỏ qua |
| Giám sát | Tỉ lệ từ chối khi ngừng, kênh không có vị trí mặc định, kiểm tra phạm vi danh sách liên-cửa-hàng |
Tham chiếu
- URD: Sản phẩm - SCH · PRD · ACC
- PRD liên quan: Danh mục sản phẩm, biến thể & định danh · Hệ thống Fare & Định giá
- Module: Sản phẩm - URD
- Developer: @nx/commerce · @nx/core · @nx/search
Rủi ro & Câu hỏi mở
| # | Rủi ro / câu hỏi | Giảm thiểu / trạng thái |
|---|---|---|
| 01 | Một kênh không có nguồn kho | Vị trí ưu tiên thấp nhất trong chuỗi là mặc định dự kiến và trùng lặp trong đầu vào gộp về lần đầu; tuy nhiên khi chạy, tồn lấy từ vị trí kho ghi trên kênh, nếu không có thì lùi về mặc định của merchant (chuỗi chưa được đọc) |
| 02 | Ngừng một kênh giữa ca | Bị từ chối khi còn đơn đang hoạt động; chủ phải đóng hoặc hủy chúng trước |
| 03 | Rò rỉ thực đơn liên-cửa-hàng | Mọi danh sách/đếm/tìm kiếm xác định quyền của người gọi rồi lọc về chúng; chỉ admin bỏ qua |
| 04 | Trùng slug trong một merchant | Bị từ chối trước khi lưu; slug duy nhất theo merchant giữa các kênh chưa xóa, không phân biệt trạng thái vòng đời - một kênh đã Ngừng hoặc Lưu trữ vẫn giữ chỗ slug |
| 05 | Dò chuỗi kho như một fallback | Chưa có - hiện chuỗi ưu tiên hoàn toàn không được đọc khi chạy; tồn lấy từ vị trí kho ghi trên kênh, nếu không có thì lùi về mặc định của merchant, còn chuỗi theo thứ tự được giữ cho logic xuất hàng sau này |
Câu hỏi thường gặp
| # | Câu hỏi | Trả lời |
|---|---|---|
| 01 | Làm sao ẩn một sản phẩm khỏi kênh này nhưng không khỏi kênh khác? | Chỉ gán sản phẩm vào những kênh nó nên xuất hiện - việc gán là công tắc hiển thị. Bỏ gán sẽ loại nó khỏi kênh đó mà không động đến các kênh khác. |
| 02 | Vị trí kho "mặc định" của một kênh là gì? | Về khái niệm, là vị trí đầu chuỗi - giá trị ưu tiên thấp nhất, có thể sắp xếp lại khi cập nhật. Nhưng hiện chuỗi chưa được đọc khi xuất hàng: đơn lấy tồn từ vị trí kho ghi trên kênh, nếu không có thì lùi về mặc định của merchant; chuỗi đã ghi dành cho logic xuất hàng sau này. |
| 03 | Tại sao tôi không ngừng được một kênh? | Vì nó vẫn còn đơn đang hoạt động. Đóng hoặc hủy chúng trước, rồi mới ngừng. |
| 04 | Admin có thấy kênh của mọi merchant không? | Có - admin và super-admin bỏ qua lọc theo vai trò; mọi người còn lại chỉ thấy kênh của các merchant họ được cấp quyền. |
| 05 | Kênh có lồng nhau được không? | Được - một kênh có thể có cha, nên các bề mặt thực đơn có thể gom nhóm. |