Mô hình miền
Bảng sale nằm trong schema PostgreSQL
sale; bảng allocation trong schemaallocation. Tất cả schema định nghĩa trong@nx/core/src/models/schemas/{sale,allocation}/. Cột số dùngdecimal(15, 4).
1. ERD đầy đủ
2. Cột chung
| Cột | Kiểu | Ghi chú |
|---|---|---|
id | text | PK, Snowflake |
createdAt / modifiedAt | timestamptz | - |
createdBy / modifiedBy | text | Audit user |
deletedAt | timestamptz | Soft-delete |
metadata | jsonb | Túi mở rộng |
3. Thực thể
3.1 SaleOrder
| Thuộc tính | Giá trị |
|---|---|
| Bảng | SaleOrder |
| Nguồn | core/src/models/schemas/sale/sale-order/schema.ts |
| Soft-delete | có |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
orderNumber | text | ✓ | Unique partial theo merchant |
name / slug | text | Slug unique partial | |
validity | jsonb | { from, to } cho order giới hạn thời gian | |
status | text | ✓ | Xem §4.1; mặc định DRAFT |
draftAt / processingAt / partialAt / completedAt / cancelledAt | timestamptz | Timestamp theo trạng thái | |
cancellationReason | text | - | |
customerId | text | FK | |
merchantId | text | ✓ | Chủ sở hữu |
saleChannelId | text | ✓ | FK |
shiftId | text | FK đến Shift | |
openedInShiftEnrollmentId / closedInShiftEnrollmentId | text | FK đến ShiftEnrollment | |
currency | text | ✓ | Mặc định VND |
exchangeRate | decimal(12,6) | Mặc định 1 | |
subtotal / tax / discount / total | decimal(15,4) | ✓ | Mặc định 0; duy trì bởi updateSummaryFromItems |
originOrderId | text | Theo dõi parent của order-split | |
checkSplitAt / orderSplitAt / mergedAt | timestamptz | Timestamp thao tác | |
counter | jsonb | { paid, paidItemIds[], total } - tiến trình thanh toán |
3.2 SaleOrderItem
| Thuộc tính | Giá trị |
|---|---|
| Bảng | SaleOrderItem |
| Polymorphic | (itemType, itemId) qua generatePrincipalColumnDefs({ discriminator: 'item', defaultPolymorphic: 'ProductVariant' }) |
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
saleOrderId | text | ✓ | FK |
itemType | text | ✓ | PRODUCT_VARIANT (mặc định) / khác |
itemId | text | ✓ | FK target |
mode | text | ✓ | PRODUCT (mặc định - auto-merge duplicate) / CUSTOM (luôn dòng mới) |
leadItemId | text | Group lead cho combo items | |
currency | text | ✓ | Mặc định VND |
basePrice / unitPrice | decimal(15,4) | ✓ | Trước/sau giảm giá, trên mỗi đơn vị |
discount / tax | decimal(15,4) | ✓ | Mặc định 0 |
quantity | decimal(15,4) | ✓ | Mặc định 1 |
total | decimal(15,4) | ✓ | Tính toán |
fareId / fareProvider | text | Tham chiếu nguồn pricing | |
priceMetadata | jsonb | Snapshot pricing (chi tiết v2) | |
transferHistory | jsonb | Array<TTransferHistoryEntry> - theo dõi merge/split | |
recipeId | text | Liên kết MaterialRecipe.id đang hoạt động (snapshot) |
3.3 SaleCheck / SaleCheckItem
SaleCheck:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
saleOrderId | text | ✓ | Order parent |
status | text | ✓ | PROCESSING (mặc định) / COMPLETED / CANCELLED |
subtotal / tax / discount / total | decimal(15,4) | ✓ | Tính lại bởi recalculateTotals |
customerId | text | Khách theo từng check (khác khách của order) |
SaleCheckItem: saleCheckId, saleOrderItemId, quantity, subtotal/tax/discount/total.
3.4 KitchenStation / KitchenTicket / KitchenTicketItem
KitchenStation: merchantId, name (i18n), status (mặc định ACTIVATED).
KitchenTicket:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
ticketNumber | text | ✓ | Unique partial; tuần tự theo station |
saleOrderId | text | ✓ | FK |
merchantId | text | ✓ | Chủ sở hữu |
kitchenStationId | text | Mục tiêu định tuyến (tùy chọn) | |
status | text | ✓ | Xem §4.3; mặc định PENDING |
priority | int | ✓ | Mặc định 0 (cờ rush làm tăng priority) |
sequence | int | ✓ | Mặc định 1 (gợi ý thứ tự) |
pendingAt / processingAt / readyAt / completedAt / voidedAt | timestamptz | Timestamp trạng thái |
KitchenTicketItem:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
kitchenTicketId | text | ✓ | FK |
saleOrderItemId | text | ✓ | FK |
quantity | decimal(15,4) | ✓ | Mặc định 1 |
status | text | ✓ | Xem §4.4; mặc định PENDING |
startedAt / readyAt / servedAt / voidedAt | timestamptz | Timestamp trạng thái |
Mỗi lần đổi trạng thái phát Kafka
KITCHEN_TICKET_ITEM_STATUS_CHANGEDvà kích hoạt đánh giá auto-progression của ticket.
3.5 AllocationUsage / AllocationUnit / AllocationZone / AllocationLayout
Schema nằm trong schema
allocation(tách khỏisale).
AllocationUsage - sử dụng đa hình của một allocation unit:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
usageType | text | ✓ | SALE_ORDER / RESERVATION (qua discriminator: 'usage') |
usageId | text | ✓ | Id FK target |
unitId | text | ✓ | FK đến AllocationUnit |
merchantId | text | ✓ | Chủ sở hữu |
assigneeId | text | Người/staff được gán | |
status | text | ✓ | ACTIVE (mặc định) / SUCCESS / CANCELLED / EXPIRED |
type | text | ✓ | GENERAL (mặc định) / DINE_IN / TAKEAWAY / DELIVERY |
reservedFrom / reservedTo / reservedAt / startedAt / completedAt | timestamptz | Timestamp vòng đời |
AllocationUnit - đơn vị vật lý (bàn, ghế, tủ):
name (i18n),zoneId(notNull),placement(jsonb vị trí),style(jsonb),capacity(int),status.
AllocationZone - section / floor / area:
name (i18n),layoutId(notNull),style(jsonb),parentId(cấp bậc tự tham chiếu),status.
AllocationLayout - container floor plan cấp cao nhất.
3.6 Reservation
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Chủ sở hữu |
guestName | text | ✓ | - |
guestPhone | text | ✓ | - |
guestEmail | text | - | |
partySize | int | ✓ | - |
reservedFrom | timestamptz | ✓ | - |
reservedTo | timestamptz | - | |
notes | text | - | |
source | text | ✓ | Mặc định PHONE; WEB / WALK_IN / APP |
occasion | text | Sinh nhật / kỷ niệm / v.v. | |
status | text | ✓ | PENDING (mặc định) / CONFIRMED / CHECKED_IN / CANCELLED |
confirmedAt / checkedInAt / cancelledAt | timestamptz | - | |
cancellationReason | text | - | |
saleOrderId | text | FK sau check-in |
3.7 Shift / ShiftDrawer / ShiftEnrollment / ShiftEnrollmentEvent / ShiftReport
Mô hình Shift v2. Nguồn:
core/src/models/schemas/sale/shift*. Tất cả thực thể đều soft-delete.
Shift - aggregate ca cấp cao nhất, scope theo (merchantId, saleChannelId):
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
merchantId | text | ✓ | Chủ sở hữu |
saleChannelId | text | ✓ | Scope kênh bán |
code | text | ✓ | Định danh (prefix SHIFT) |
name | jsonb | i18n { default, en?, vi? } | |
notes | jsonb | i18n { default, en?, vi? } | |
status | text | ✓ | Xem §4.7; mặc định 100_OPENING |
openedById | text | ✓ | User mở ca |
closedById | text | User đóng ca | |
openedAt / closedAt | timestamptz | openedAt mặc định là hiện tại | |
startAt / endAt | timestamptz | Khung giờ dự kiến (tùy chọn) | |
metadata | jsonb | Tự do (key cũ openNotes/closeNotes đã gỡ - ghi chú nằm ở cột notes) |
Một kênh có thể có nhiều ca
100_OPENINGcùng lúc (mỗi thiết bị chỉ ở 1 ca mở - chặn qua enrollment);(merchantId, saleChannelId)là index thường.
ShiftDrawer - ngăn kéo tiền theo thiết bị, 1:1 với một ShiftEnrollment:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
shiftEnrollmentId | text | ✓ | Chủ 1:1 (partial unique) |
shiftId / merchantId / deviceId | text | ✓ | Scope |
status | text | ✓ | Cùng bộ với Shift; mặc định 100_OPENING |
openingFloat | decimal(15,4) | ✓ | Tiền float lúc mở |
expectedCash / actualCash / cashDiscrepancy | decimal(15,4) | Đối chiếu | |
expectedNonCash / actualNonCash | jsonb | TShiftNonCashBreakdown (theo phương thức non-cash) | |
closeRecountCount | int | ✓ | Mặc định 0; số lần đếm lại |
openedAt / closedAt | timestamptz | openedAt mặc định là hiện tại |
ShiftEnrollment - một principal (device hoặc user) tham gia vào một Shift:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
shiftId / merchantId | text | ✓ | Scope |
principalType | text | ✓ | Device / User |
principalId | text | ✓ | Id device hoặc user |
status | text | ✓ | ACTIVATED (mặc định) / DEACTIVATED |
role | text | Vai trò trong ca | |
drawerId | text | Phiên ngăn kéo dùng chung đang gắn | |
enrolledById | text | - | |
enrolledAt / leftAt | timestamptz | enrolledAt mặc định là hiện tại | |
notes | jsonb | i18n { default, en?, vi? } - ghi chú lúc join/tạo ca |
Nhiều nhất một enrollment
ACTIVATEDcho mỗi principal - partial unique index.
ShiftEnrollmentEvent - ledger chỉ-ghi-thêm của các sự kiện enrollment + cash-movement:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
shiftEnrollmentId / shiftId / merchantId | text | ✓ | Scope |
type | text | ✓ | Xem §4.8 |
amount | decimal(15,4) | Chỉ set cho cash movement (PAY_IN / PAY_OUT / SAFE_DROP) | |
occurredAt | timestamptz | ✓ | Mặc định là hiện tại |
note | text | - |
ShiftReport - các dòng báo cáo X/Z; theo từng enrollment khi có shiftEnrollmentId, toàn ca khi NULL:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
shiftId / merchantId / saleChannelId | text | ✓ | Scope |
shiftEnrollmentId | text | NULL = tổng hợp toàn ca; có giá trị = theo từng enrollment | |
reportType | text | ✓ | X (snapshot trực tiếp) / Z (cuối cùng) |
reportNo | int | ✓ | Tuần tự theo scope |
openedById / closedById / generatedById | text | Audit | |
openedAt / closedAt / generatedAt | timestamptz | generatedAt mặc định là hiện tại | |
openingFloat, cashSales, cashRefunds, payIns, payOuts, safeDrops, expectedCash, actualCash, cashDiscrepancy | decimal(15,4) | ✓ | Tổng kết tiền mặt |
grossSales, discounts, taxAmount, netSales, voidAmount | decimal(15,4) | ✓ | Tổng kết doanh số |
orderCount / itemCount / voidCount | int | ✓ | Mặc định 0 |
paymentMethods / sections / categoryBreakdown / actualNonCash | jsonb | Các phân rã | |
notes | text | - |
3.8 Customer
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
name | text | ✓ | - |
phone | text | - | |
email | text | - | |
userId | text | Tài khoản user được liên kết (tùy chọn) | |
merchantId | text | ✓ | Chủ sở hữu |
pointBalance | decimal(15,4) | ✓ | Mặc định 0 |
3.9 PointTransaction
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
customerId | text | ✓ | FK |
merchantId | text | ✓ | Chủ sở hữu |
saleOrderId | text | ✓ | Order nguồn |
type | text | ✓ | AWARD / REDEEM / ADJUST (theo PointTransactionTypes) |
points | decimal(15,4) | ✓ | Lượng chênh lệch có dấu |
conversionRate | decimal(15,4) | ✓ | Snapshot tỷ lệ điểm trên tiền tệ tại thời điểm cộng điểm |
Tính idempotent:
PointTransactionRepository.existsBySaleOrderIdchặn cộng điểm trùng lặp.
4. Status Enums
4.1 SaleOrderStatuses
| Giá trị | Giai đoạn |
|---|---|
DRAFT | Giỏ hàng / item còn sửa được |
PROCESSING | Checkout xong, chờ thanh toán |
PARTIAL | Đã nhận một phần thanh toán |
COMPLETED | Đã thanh toán đủ |
CANCELLED | Trạng thái cuối |
4.2 SaleCheckStatuses
| Giá trị | Giai đoạn |
|---|---|
PROCESSING | Mặc định - đang nhận thanh toán |
PARTIAL | Đã nhận một phần thanh toán |
COMPLETED | Đã thanh toán đủ |
CANCELLED | Trạng thái cuối |
4.3 KitchenTicketStatuses
Nguồn:
core/src/models/schemas/sale/kitchen-ticket/constants.ts. Ticket ở lớp khác với các item - chúng có bộ status khác nhau.
| Giá trị | Code | Giai đoạn |
|---|---|---|
PENDING | 103_PENDING | Vừa gửi xuống bếp, chưa item nào COOKING |
PROCESSING | 203_PROCESSING | Ít nhất một item đang COOKING (auto-progress từ PENDING) |
READY | 302_SUCCESS | Tất cả item READY-trở-đi (auto) |
COMPLETED | 303_COMPLETED | Tất cả item ở trạng thái cuối, ≥1 SERVED (auto) |
VOIDED | 505_CANCELLED | Void thủ công |
Các guard helper: canVoid (bất kỳ trạng thái hoạt động nào), canProgress (chỉ PENDING), canMarkReady (chỉ PROCESSING), canComplete (chỉ READY).
4.4 KitchenTicketItemStatuses
Nguồn:
core/src/models/schemas/sale/kitchen-ticket-item/constants.ts. Bộ khác với enum cấp ticket.
| Giá trị | Code | Trigger |
|---|---|---|
PENDING | 103_PENDING | Khởi tạo |
COOKING | 203_PROCESSING | startCookingItem |
READY | 302_SUCCESS | markItemReady - phát Kafka KITCHEN_TICKET_ITEM_STATUS_CHANGED |
SERVED | 303_COMPLETED | markItemServed |
VOIDED | 505_CANCELLED | voidTicketItem |
4.5 AllocationUsageStatuses
| Giá trị | Giai đoạn |
|---|---|
ACTIVE | Đang giữ chỗ/đang chiếm dụng |
SUCCESS | Order đã thanh toán → usage đóng |
CANCELLED | Order/reservation đã huỷ |
EXPIRED | Reservation quá hạn |
4.6 ReservationStatuses
| Giá trị | Giai đoạn |
|---|---|
PENDING | Đã tạo, chờ xác nhận |
CONFIRMED | Đã được chủ đặt chỗ xác nhận |
CHECKED_IN | Khách đã đến; sinh SaleOrder |
CANCELLED | Trạng thái cuối |
4.7 ShiftStatuses
Nguồn:
core/src/models/schemas/sale/shift/constants.ts.ShiftDrawerStatusesdùng bộ giống hệt;isOpen=100_OPENING.
| Giá trị | Giai đoạn |
|---|---|
100_OPENING | Ca / drawer đang hoạt động |
200_CLOSED | Đã đóng, chờ đối chiếu |
300_RECONCILED | Đã đối chiếu tiền - trạng thái cuối |
4.8 ShiftEnrollment Enums
ShiftEnrollmentStatuses: ACTIVATED (mặc định) ↔ DEACTIVATED.
ShiftPrincipalType: Device / User.
ShiftEnrollmentEventTypes - ledger chỉ-ghi-thêm; amount chỉ set cho cash movement:
| Giá trị | Cash movement |
|---|---|
DROP_IN / DROP_OUT | không |
CHECK_IN / CHECK_OUT | không |
DRAWER_OPEN / RECONCILE | không |
PAY_IN / PAY_OUT / SAFE_DROP | có (amount bắt buộc) |
4.9 ShiftReportTypes
| Giá trị | Giai đoạn |
|---|---|
X | Snapshot trực tiếp (chạy lại được) |
Z | Báo cáo cuối cùng - một cho mỗi scope |
5. Bất biến liên thực thể
| Bất biến | Áp đặt |
|---|---|
SaleOrder.subtotal/tax/discount/total = Σ(items) | Service updateSummaryFromItems sau mỗi lần thay đổi item |
Nhiều nhất một Shift 100_OPENING cho mỗi (merchantId, saleChannelId) | Partial unique index + ShiftService |
Nhiều nhất một ShiftEnrollment ACTIVATED cho mỗi principal | Partial unique index |
ShiftDrawer 1:1 với một ShiftEnrollment | Partial unique index trên shiftEnrollmentId |
ShiftEnrollmentEvent.amount chỉ set cho PAY_IN / PAY_OUT / SAFE_DROP | ShiftEnrollmentEventTypes.isCashMovement |
Tổng SaleCheck = Σ(SaleCheckItem) cho dòng của nó | SaleCheckRepository.recalculateTotals |
KitchenTicket auto-progress qua PENDING→COOKING→READY→SERVED dựa trên status item | KitchenTicketRepository.evaluateTicketAutoProgression sau mỗi lần đổi status item |
KitchenTicket.ticketNumber unique partial theo kitchen station (sequence reset khi đổi station) | Schema partial unique + getNextSequence |
PointTransaction idempotent theo (customerId, saleOrderId) | Tra cứu existsBySaleOrderId trước khi ghi |
AllocationUsage theo vòng đời order/reservation (huỷ lan truyền) | Ở cấp service trên cancelOrder / huỷ |
Thao tác merge / split order bảo toàn tổng item | Transaction cấp service; audit transferHistory |
6. Hành vi Soft-delete
| Thực thể | Soft-delete | Ghi chú |
|---|---|---|
| Tất cả thực thể sale | ✓ | Marker deletedAt; archive cho SaleOrder = soft-delete |
KitchenTicket voidedAt | logical | voidedAt không phải soft-delete; ticket vẫn truy vấn được |