Skip to content

Quản lý Phiếu Bếp

1. Tổng quan

Hệ thống Quản lý Phiếu Bếp kết nối giữa POS phía trước quầy (FOH — Front of House) và hệ thống hiển thị bếp phía sau (BOH — Back of House / KDS). Khi nhân viên phục vụ gửi món đến bếp, một KitchenTicket được tạo ra chứa một hoặc nhiều KitchenTicketItem. Nhân viên bếp chuyển trạng thái từng món qua các giai đoạn chế biến, và trạng thái phiếu tự động cập nhật dựa trên trạng thái các món.

Mã nguồn: Schema trong @nx/core (packages/core/src/models/schemas/sale/kitchen-ticket/, kitchen-ticket-item/).

Nguyên tắc thiết kế

Nguyên tắcMô tả
Hủy và gửi lạiSau khi gửi, không thể sửa trực tiếp món trong phiếu. Mọi thay đổi (số lượng, modifier, thay thế món) đều theo mô hình hủy + gửi lại — hủy phiếu/món cũ, rồi gửi phiếu mới với dữ liệu đúng
Tự động chuyển trạng tháiTrạng thái phiếu được tính tự động từ trạng thái các món qua evaluateTicketAutoProgression — không cần quản lý trạng thái phiếu thủ công
Gửi idempotentidempotencyKey tùy chọn ngăn tạo phiếu trùng khi retry

So sánh với ngành

Phương pháp hủy-và-gửi-lại là tiêu chuẩn ngành trên các nền tảng KDS lớn. Trong quá trình thiết kế, chúng tôi đã nghiên cứu và so sánh với 6 hệ thống KDS thương mại:

Hệ thốngThị trườngĐiểm tương đồng
Toast POSDẫn đầu thị trường Mỹ (127K+ nhà hàng)Cloud-based, tự động chuyển trạng thái từ trạm bếp đến phục vụ
Square KDSSMBKhông thể sửa sau khi gửi — hủy và nhập lại
Lightspeed RestaurantMid-market, toàn cầuKhông thể sửa — hủy và nhập lại
Oracle MICROS SimphonyChuỗi doanh nghiệpKDS Controller điều phối tập trung
Aloha POS (NCR Voyix)Nhà hàng full-serviceBắt buộc xác nhận hủy trên KDS, hủy và nhập lại
Odoo POSMã nguồn mở, tích hợp ERPChuyển trạng thái thủ công

Kết quả chính:

  • 5/6 hệ thống dùng hủy-và-gửi-lại cho thay đổi số lượng/modifier (chỉ Toast hỗ trợ sửa trực tiếp real-time)
  • 5 trạng thái món (PENDING → COOKING → READY → SERVED + VOIDED) chi tiết hơn 3–4 trạng thái thông thường
  • evaluateTicketAutoProgression của chúng tôi tinh vi hơn hầu hết các hệ thống về tự động chuyển trạng thái từ trạm bếp đến phục vụ
  • Điều phối bằng transaction tương đương kiến trúc KDS Controller của Oracle hay KDS Master của Aloha

2. Sơ đồ quan hệ thực thể

3. Vòng đời trạng thái

3.1. Trạng thái KitchenTicketItem

Trạng tháiGiá trị nội bộChuyển từChuyển đến
PENDING103_PENDINGTạo mớiCOOKING, VOIDED
COOKING203_PROCESSINGPENDINGREADY, VOIDED
READY302_SUCCESSCOOKINGSERVED, VOIDED
SERVED303_COMPLETEDREADYKết thúc
VOIDED505_CANCELLEDPENDING, COOKING, READYKết thúc

3.2. Trạng thái KitchenTicket (Tự động chuyển)

Trạng thái phiếu không bao giờ được đặt trực tiếp — được tính bởi evaluateTicketAutoProgression dựa trên tổng hợp trạng thái các món:

Trạng tháiGiá trị nội bộĐiều kiện kích hoạt
PENDING103_PENDINGTạo mới, chưa có món nào bắt đầu
PROCESSING203_PROCESSINGBất kỳ món nào đang COOKING, READY, hoặc SERVED
READY302_SUCCESSTất cả món active là READY, SERVED, hoặc VOIDED
COMPLETED303_COMPLETEDTất cả món kết thúc, ít nhất 1 SERVED
VOIDED505_CANCELLEDTẤT CẢ món VOIDED, không có SERVED

IMPORTANT

Phiếu có hỗn hợp SERVED + VOIDED sẽ thành COMPLETED, không phải VOIDED. Trạng thái VOIDED chỉ áp dụng khi tất cả các món đều bị hủy.

4. REST API

Đường dẫn gốc: /v1/api/sale

4.1. Endpoint FOH (POS Terminal)

Gửi đến bếp

Tạo phiếu bếp mới với các món từ đơn hàng.

POST /sale-orders/{saleOrderId}/kitchen-tickets/send
TrườngKiểuBắt buộcMô tả
itemsArray<{ saleOrderItemId, quantity }>Các món gửi (quantity dạng string)
kitchenStationIdstringKhôngTrạm bếp đích
prioritynumberKhông0=bình thường (mặc định), 1=ưu tiên
notestringKhôngGhi chú hiển thị trên KDS (tối đa 500 ký tự)
idempotencyKeystringKhôngNgăn tạo phiếu trùng khi retry (tối đa 100 ký tự)

Response: KitchenTicket với mảng items.

Idempotency: Nếu idempotencyKey trùng với phiếu hiện có của cùng đơn hàng, phiếu hiện tại được trả về mà không tạo mới.

Hủy phiếu

Hủy tất cả món chưa kết thúc trong phiếu. Món SERVED không bị ảnh hưởng.

POST /kitchen-tickets/{ticketId}/void
TrườngKiểuBắt buộcMô tả
reasonstringKhôngLý do hủy (tối đa 500 ký tự)

Response: KitchenTicket — trạng thái là VOIDED nếu tất cả món bị hủy, COMPLETED nếu một số đã SERVED.

Ưu tiên phiếu

Đánh dấu phiếu là ưu tiên cao.

POST /kitchen-tickets/{ticketId}/rush
TrườngKiểuBắt buộcMô tả
reasonstringKhôngLý do ưu tiên (tối đa 500 ký tự)

Response: KitchenTicket với priority: 1metadata.rushReason.

Hủy một món

POST /kitchen-ticket-items/{itemId}/void
TrườngKiểuBắt buộcMô tả
reasonstringKhôngLý do hủy

Response: KitchenTicketItem với trạng thái VOIDED. Tự động cập nhật trạng thái phiếu cha.

4.2. Endpoint BOH (KDS)

Chuyển trạng thái từng món

POST /kitchen-ticket-items/{itemId}/start-cooking   # PENDING → COOKING
POST /kitchen-ticket-items/{itemId}/ready            # COOKING → READY
POST /kitchen-ticket-items/{itemId}/served           # READY → SERVED

Không cần body. Mỗi endpoint trả về KitchenTicketItem đã cập nhật và tự động chuyển trạng thái phiếu cha.

Thao tác hàng loạt trên phiếu

POST /kitchen-tickets/{ticketId}/ready    # Tất cả PENDING/COOKING → READY
POST /kitchen-tickets/{ticketId}/served   # Tất cả READY → SERVED

Không cần body. Trả về KitchenTicket đã cập nhật với danh sách món.

4.3. Endpoint đọc dữ liệu

MethodĐường dẫnMô tả
GET/kitchen-ticketsDanh sách phiếu (filter, include, phân trang)
GET/kitchen-tickets/{id}Lấy phiếu theo ID
GET/kitchen-ticket-itemsDanh sách món (filter, phân trang)
GET/kitchen-ticket-items/{id}Lấy món theo ID

Bộ lọc thường dùng:

GET /kitchen-tickets?filter[saleOrderId]=xxx&include=items
GET /kitchen-tickets?filter[kitchenStationId]=xxx&filter[status][$ne]=505_CANCELLED
GET /kitchen-ticket-items?filter[kitchenTicketId]=xxx

4.4. Tóm tắt endpoint

FOH chỉ có 3 thao tác ghi cho phiếu:

Thao tácChức năng
GửiTạo phiếu mới với các món
Hủy phiếuHủy tất cả món active trong phiếu
Hủy mónHủy một món đơn lẻ

Mọi thao tác khác (thay đổi số lượng, modifier, thay thế món, gửi lại) đều là tổ hợp hủy + gửi. Đây là cách tiếp cận tương tự Square, Lightspeed, Aloha, và Oracle MICROS.

5. Sự kiện WebSocket

5.1. Topic

TopicGiá trị
Phiếu bếpwt:observation/sale/kitchen-ticket
Món trong phiếuwt:observation/sale/kitchen-ticket-item

5.2. Room

Sự kiện được phát đến nhiều room đồng thời:

RoomPatternNgười đăng ký
Merchant (tất cả)wr:observation/merchants/{merchantId}Dashboard
Merchant Kitchenwr:observation/merchants/{merchantId}/kitchenTất cả màn hình KDS
Order Kitchenwr:observation/sale-orders/{saleOrderId}/kitchenChi tiết đơn POS
Stationwr:observation/kitchen-stations/{stationId}KDS theo trạm
Ticketwr:observation/kitchen-tickets/{ticketId}Chi tiết phiếu
Ticket Itemswr:observation/kitchen-tickets/{ticketId}/itemsSự kiện cấp món
Specific Itemwr:observation/kitchen-ticket-items/{itemId}Một món cụ thể

5.3. Action

ActionKích hoạt bởiPayload
TICKET_CREATEDsendToKitchen{ ticket, items[], merchantId }
TICKET_VOIDEDvoidTicket{ ticket, items[], merchantId }
TICKET_RUSHEDrushTicket{ ticket, items[], merchantId }
TICKET_STATUS_CHANGEDmarkTicketReady, markTicketServed{ ticket, items[], merchantId }
ITEM_VOIDEDvoidTicketItem{ ticket, item, merchantId }
ITEM_STATUS_CHANGEDstartCookingItem, markItemReady, markItemServed{ ticket, item, merchantId }

6. Service

6.1. KitchenTicketService

Mã nguồn: src/services/kitchen-ticket.service.tsDI Dependencies: SaleOrderRepository, SaleOrderItemRepository, KitchenStationRepository, KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService

Phương thứcMô tả
sendToKitchenXác thực đơn + món + trạm, tạo phiếu + món trong transaction. Hỗ trợ idempotency key để ngăn phiếu trùng.
voidTicketHủy tất cả món active (PENDING/COOKING/READY) trong một câu SQL. Món SERVED không bị ảnh hưởng. Tự động cập nhật trạng thái phiếu. Lưu lý do hủy vào metadata.
rushTicketĐặt priority=1 và lưu lý do ưu tiên. Không cần transaction (update đơn giản).
markTicketReadyCập nhật hàng loạt tất cả món PENDING/COOKING → READY. Tự động cập nhật trạng thái phiếu.
markTicketServedCập nhật hàng loạt tất cả món READY → SERVED. Tự động cập nhật trạng thái phiếu.

6.2. KitchenTicketItemService

Mã nguồn: src/services/kitchen-ticket-item.service.tsDI Dependencies: KitchenTicketRepository, KitchenTicketItemRepository, SaleSocketEventService

Phương thứcMô tả
voidTicketItemHủy một món (kiểm tra canVoid). Tự động cập nhật trạng thái phiếu cha.
startCookingItemPENDING → COOKING. Đặt startedAt. Tự động chuyển phiếu sang PROCESSING.
markItemReadyCOOKING → READY. Đặt readyAt. Có thể tự động chuyển phiếu sang READY.
markItemServedREADY → SERVED. Đặt servedAt. Có thể tự động chuyển phiếu sang COMPLETED.

Tất cả chuyển trạng thái món dùng chung helper _transitionItemStatus để xác thực guard, cập nhật món, và gọi evaluateTicketAutoProgression.

7. Repository

RepositoryThực thểPhương thức đặc biệt
KitchenTicketRepositoryKitchenTicketgetNextSequence, findByIdempotencyKey, evaluateTicketAutoProgression
KitchenTicketItemRepositoryKitchenTicketItemfindByTicketId, findBySaleOrderItemId, findActiveByTicketId
KitchenStationRepositoryKitchenStationCRUD chuẩn

evaluateTicketAutoProgression

Mã nguồn: packages/core/src/repositories/sale/kitchen-ticket.repository.ts

Chạy 4 câu SQL cập nhật có điều kiện theo thứ tự ưu tiên (tất cả trong cùng transaction):

  1. Tất cả món VOIDED → phiếu VOIDED
  2. Tất cả SERVED/VOIDED (1+ SERVED) → phiếu COMPLETED
  3. Tất cả READY/SERVED/VOIDED → phiếu READY
  4. Bất kỳ COOKING/READY/SERVED → phiếu PROCESSING
  5. Còn lại → không thay đổi (vẫn PENDING)

Mỗi bước dùng subquery EXISTS/NOT EXISTS để tránh race condition.

8. Quyền hạn

ActionMô tả
KitchenTicket.sendCREATEGửi món đến bếp
KitchenTicket.voidDELETEHủy phiếu bếp
KitchenTicket.rushUPDATEĐánh dấu phiếu ưu tiên
KitchenTicket.updateTicketStatusUPDATECập nhật trạng thái hàng loạt (KDS)
KitchenTicketItem.voidItemDELETEHủy một món
KitchenTicketItem.updateItemStatusUPDATEChuyển trạng thái món (KDS)

9. Kịch bản vận hành

9.1. Luồng bình thường

Phục vụ: Tạo SaleOrder → Thêm món
Phục vụ: POST /sale-orders/{id}/kitchen-tickets/send
  → Phiếu PENDING, tất cả món PENDING

Đầu bếp: POST /kitchen-ticket-items/{id}/start-cooking  (từng món)
  → Món COOKING, phiếu tự động → PROCESSING

Đầu bếp: POST /kitchen-ticket-items/{id}/ready  (từng món)
  → Món READY, khi tất cả ready → phiếu tự động → READY

Phục vụ: POST /kitchen-ticket-items/{id}/served  (từng món)
  → Món SERVED, khi tất cả served → phiếu tự động → COMPLETED

9.2. Gửi theo đợt

Phục vụ: Gửi khai vị → Phiếu #1 (sequence: 1)
  ...khách ăn xong...
Phục vụ: Gửi món chính → Phiếu #2 (sequence: 2)

Mỗi lần gọi sendToKitchen tạo phiếu mới. Trường sequence tự tăng theo SaleOrder.

9.3. Thay đổi số lượng sau khi gửi (Hủy + Gửi lại)

Mọi thay đổi số lượng đều theo mô hình hủy + gửi lại:

Khách: "Đổi 3 Burger thành 1 Burger"

Cách A — Hủy toàn bộ phiếu và gửi lại:
  Phục vụ: POST /kitchen-tickets/{ticket1Id}/void
            { "reason": "Đổi số lượng — khách muốn 1 burger" }
  Phục vụ: POST /sale-orders/{id}/kitchen-tickets/send
            { "items": [{ "saleOrderItemId": "burger-id", "quantity": "1" }] }

Cách B — Hủy từng món (nếu phiếu có món khác cần giữ):
  Phục vụ: POST /kitchen-ticket-items/{burgerItemId}/void
            { "reason": "Đổi số lượng" }
  Phục vụ: POST /sale-orders/{id}/kitchen-tickets/send
            { "items": [{ "saleOrderItemId": "burger-id", "quantity": "1" }] }

TIP

FE nên kiểm tra trạng thái món trước khi hủy. Nếu món đang COOKING hoặc READY, cảnh báo người dùng ("Món đang được chế biến. Vẫn hủy?"). Món SERVED không thể hủy.

9.4. Thay thế món

Phục vụ: POST /kitchen-ticket-items/{burgerId}/void  { reason: "Đổi sang gà" }
Phục vụ: POST /sale-orders/{id}/kitchen-tickets/send  { items: [chicken] }

9.5. Gửi lại (Làm lại)

Phục vụ: POST /kitchen-ticket-items/{originalId}/void  { reason: "Rơi đĩa" }
Phục vụ: POST /sale-orders/{id}/kitchen-tickets/send
          { items: [cùng món], priority: 1, note: "LÀM LẠI — rơi đĩa" }

Đặt priority: 1 để ưu tiên. Trường note hiển thị trên KDS.

9.6. Hủy toàn bộ phiếu

Phục vụ: POST /kitchen-tickets/{ticketId}/void  { reason: "Bàn hủy đơn" }

Hệ thống: Tất cả PENDING/COOKING/READY → VOIDED
          Món SERVED không bị ảnh hưởng
          Nếu tất cả VOIDED → phiếu VOIDED
          Nếu hỗn hợp SERVED+VOIDED → phiếu COMPLETED

9.7. Cây quyết định

Phục vụ muốn thay đổi sau khi gửi?

├── Đổi số lượng (tăng hoặc giảm)?
│   └── Hủy phiếu → gửi lại với số lượng mới
│       (hoặc hủy từng món nếu phiếu có món khác cần giữ)

├── Xóa món hoàn toàn?
│   └── Hủy món (hoặc hủy phiếu nếu chỉ còn món đó)

├── Thay thế món?
│   └── Hủy món cũ → gửi món mới

├── Đổi modifier?
│   └── Hủy món cũ → gửi món mới với modifier đã cập nhật

├── Làm lại (re-fire)?
│   └── Hủy món cũ → gửi món mới với priority: 1

├── Ưu tiên?
│   └── POST /kitchen-tickets/{id}/rush

└── Hủy tất cả?
    └── POST /kitchen-tickets/{id}/void

10. Tích hợp Frontend

10.1. POS Terminal (FOH)

Trách nhiệmCài đặt
Gửi món chưa gửi đến bếpPOST /sale-orders/{id}/kitchen-tickets/send
Hủy món/phiếu khi khách hủyPOST /kitchen-ticket-items/{id}/void hoặc /kitchen-tickets/{id}/void
Thay đổi số lượng/modifier/thay thếHủy phiếu/món cũ → gửi lại với dữ liệu đúng
Ưu tiên phiếuPOST /kitchen-tickets/{id}/rush
Theo dõi trạng thái bếp theo đơnSubscribe wr:observation/sale-orders/{saleOrderId}/kitchen
Idempotency khi retryBao gồm idempotencyKey trong request gửi
Xác thực trước khi hủyKiểm tra trạng thái món — cảnh báo nếu COOKING/READY, chặn nếu SERVED

10.2. Màn hình KDS (BOH)

Trách nhiệmCài đặt
Danh sách phiếu active theo trạmGET /kitchen-tickets?filter[kitchenStationId]={id}&filter[status][$ne]=505_CANCELLED
Chuyển trạng thái từng mónPOST /kitchen-ticket-items/{id}/start-cooking, /ready, /served
Hoàn thành phiếu hàng loạtPOST /kitchen-tickets/{id}/ready hoặc /served
Hiển thị chỉ báo ưu tiênKiểm tra ticket.priority === 1
Hiển thị chỉ báo hủyFlash/highlight món bị hủy để đầu bếp xác nhận
Sắp xếp theo ưu tiên + thời gianSắp xếp theo priority DESC, pendingAt ASC
Cập nhật real-timeSubscribe wr:observation/kitchen-stations/{stationId}

10.3. Xử lý đồng thời

Cơ chếMô tả
Idempotency keyBao gồm idempotencyKey trong request gửi (vd: {saleOrderId}-{timestamp}-{random}) — ngăn phiếu trùng khi retry
Xác thực SaleOrderItemsendToKitchen xác thực từng saleOrderItemId tồn tại — nếu phục vụ khác đã xóa món, request trả lỗi 404
Guard trạng thái khi hủyHủy món đã VOIDED hoặc SERVED trả lỗi 400 — FE nên xử lý gracefully và refresh state từ WS
State từ WSKhi nhận sự kiện WS, refresh trạng thái phiếu/món thay vì dựa vào state cục bộ

11. Tham chiếu mô hình dữ liệu

KitchenTicket

TrườngKiểuMô tả
idstringSnowflake ID
ticketNumberstringĐịnh danh duy nhất (timestamp-snowflake)
saleOrderIdstringĐơn hàng cha
merchantIdstringMerchant sở hữu
kitchenStationIdstring?Trạm bếp được gán (nullable)
statusstringTrạng thái hiện tại (tự động chuyển)
prioritynumber0=bình thường, 1=ưu tiên
sequencenumberThứ tự trong SaleOrder
pendingAtDateThời điểm tạo
processingAtDate?Thời điểm món đầu tiên bắt đầu nấu
readyAtDate?Thời điểm tất cả món sẵn sàng
completedAtDate?Thời điểm tất cả món được phục vụ
voidedAtDate?Thời điểm hủy
metadataobject{ note?, idempotencyKey?, rushReason?, voidReason? }

KitchenTicketItem

TrườngKiểuMô tả
idstringSnowflake ID
kitchenTicketIdstringPhiếu cha
saleOrderItemIdstringMón trong đơn hàng liên kết
quantitystringSố lượng (decimal dạng string)
statusstringTrạng thái hiện tại
startedAtDate?Thời điểm bắt đầu nấu
readyAtDate?Thời điểm đánh dấu sẵn sàng
servedAtDate?Thời điểm phục vụ cho khách
voidedAtDate?Thời điểm hủy
metadataobject{ productMetadata?, modifiers?, specialInstructions?, voidReason? }

12. Tài liệu liên quan

Tài liệuMô tả
Đơn hàng BánThực thể SaleOrder, chế độ món, vòng đời trạng thái
Sự kiện Real-TimeHạ tầng WebSocket, tích hợp Signal
Luồng CheckoutLuồng checkout và thanh toán

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