Domain Model
Sale tables live in
salePostgreSQL schema; allocation tables inallocationschema. All schemas defined in@nx/core/src/models/schemas/{sale,allocation}/. Numeric columns usedecimal(15, 4).
1. Full ERD
2. Common Columns
| Column | Type | Notes |
|---|---|---|
id | text | PK, Snowflake |
createdAt / modifiedAt | timestamptz | — |
createdBy / modifiedBy | text | User audit |
deletedAt | timestamptz | Soft-delete |
metadata | jsonb | Extension bag |
3. Entities
3.1 SaleOrder
| Property | Value |
|---|---|
| Table | SaleOrder |
| Source | core/src/models/schemas/sale/sale-order/schema.ts |
| Soft-delete | yes |
| Field | Type | Required | Description |
|---|---|---|---|
orderNumber | text | ✓ | Unique partial per merchant |
name / slug | text | Slug unique partial | |
validity | jsonb | { from, to } for time-limited orders | |
status | text | ✓ | See §4.1; default DRAFT |
draftAt / processingAt / partialAt / completedAt / cancelledAt | timestamptz | Per-status timestamps | |
cancellationReason | text | — | |
customerId | text | FK | |
merchantId | text | ✓ | Owner |
saleChannelId | text | ✓ | FK |
openedInSessionId / closedInSessionId | text | FK to PosSession | |
currency | text | ✓ | Default VND |
exchangeRate | decimal(12,6) | Default 1 | |
subtotal / tax / discount / total | decimal(15,4) | ✓ | Default 0; maintained by updateSummaryFromItems |
originOrderId | text | For order-split parent tracking | |
checkSplitAt / orderSplitAt / mergedAt | timestamptz | Operation timestamps | |
counter | jsonb | { paid, paidItemIds[], total } — payment progress |
3.2 SaleOrderItem
| Property | Value |
|---|---|
| Table | SaleOrderItem |
| Polymorphic | (itemType, itemId) via generatePrincipalColumnDefs({ discriminator: 'item', defaultPolymorphic: 'ProductVariant' }) |
| Field | Type | Required | Description |
|---|---|---|---|
saleOrderId | text | ✓ | FK |
itemType | text | ✓ | PRODUCT_VARIANT (default) / other |
itemId | text | ✓ | FK target |
mode | text | ✓ | PRODUCT (default — auto-merge duplicates) / CUSTOM (always new line) |
leadItemId | text | Group lead for combo items | |
currency | text | ✓ | Default VND |
basePrice / unitPrice | decimal(15,4) | ✓ | Pre-discount / post-discount per-unit |
discount / tax | decimal(15,4) | ✓ | Default 0 |
quantity | decimal(15,4) | ✓ | Default 1 |
total | decimal(15,4) | ✓ | Computed |
fareId / fareProvider | text | Pricing source ref | |
priceMetadata | jsonb | Pricing snapshot (v2 detail) | |
transferHistory | jsonb | Array<TTransferHistoryEntry> — merge/split tracking | |
recipeId | text | Linked active MaterialRecipe.id (snapshot) |
3.3 SaleCheck / SaleCheckItem
SaleCheck:
| Field | Type | Required | Description |
|---|---|---|---|
saleOrderId | text | ✓ | Parent order |
status | text | ✓ | PROCESSING (default) / COMPLETED / CANCELLED |
subtotal / tax / discount / total | decimal(15,4) | ✓ | Recalculated by recalculateTotals |
customerId | text | Per-check customer (different from order's) |
SaleCheckItem: saleCheckId, saleOrderItemId, quantity, subtotal/tax/discount/total.
3.4 KitchenStation / KitchenTicket / KitchenTicketItem
KitchenStation: merchantId, name (i18n), status (default ACTIVATED).
KitchenTicket:
| Field | Type | Required | Description |
|---|---|---|---|
ticketNumber | text | ✓ | Unique partial; sequential per station |
saleOrderId | text | ✓ | FK |
merchantId | text | ✓ | Owner |
kitchenStationId | text | Routing target (optional) | |
status | text | ✓ | See §4.3; default PENDING |
priority | int | ✓ | Default 0 (rush flag bumps priority) |
sequence | int | ✓ | Default 1 (ordering hint) |
pendingAt / processingAt / readyAt / completedAt / voidedAt | timestamptz | Status timestamps |
KitchenTicketItem:
| Field | Type | Required | Description |
|---|---|---|---|
kitchenTicketId | text | ✓ | FK |
saleOrderItemId | text | ✓ | FK |
quantity | decimal(15,4) | ✓ | Default 1 |
status | text | ✓ | See §4.4; default PENDING |
startedAt / readyAt / servedAt / voidedAt | timestamptz | Status timestamps |
Each status change emits Kafka
KITCHEN_TICKET_ITEM_STATUS_CHANGEDand triggers ticket auto-progression evaluation.
3.5 AllocationUsage / AllocationUnit / AllocationZone / AllocationLayout
Schema lives in
allocationschema (separate fromsale).
AllocationUsage — polymorphic usage of an allocation unit:
| Field | Type | Required | Description |
|---|---|---|---|
usageType | text | ✓ | SALE_ORDER / RESERVATION (via discriminator: 'usage') |
usageId | text | ✓ | FK target id |
unitId | text | ✓ | FK to AllocationUnit |
merchantId | text | ✓ | Owner |
assigneeId | text | Person/staff assigned | |
status | text | ✓ | ACTIVE (default) / SUCCESS / CANCELLED / EXPIRED |
type | text | ✓ | GENERAL (default) / DINE_IN / TAKEAWAY / DELIVERY |
reservedFrom / reservedTo / reservedAt / startedAt / completedAt | timestamptz | Lifecycle timestamps |
AllocationUnit — physical unit (table, seat, locker):
name (i18n),zoneId(notNull),placement(jsonb position),style(jsonb),capacity(int),status.
AllocationZone — section / floor / area:
name (i18n),layoutId(notNull),style(jsonb),parentId(self-ref hierarchy),status.
AllocationLayout — top-level floor plan container.
3.6 Reservation
| Field | Type | Required | Description |
|---|---|---|---|
merchantId | text | ✓ | Owner |
guestName | text | ✓ | — |
guestPhone | text | ✓ | — |
guestEmail | text | — | |
partySize | int | ✓ | — |
reservedFrom | timestamptz | ✓ | — |
reservedTo | timestamptz | — | |
notes | text | — | |
source | text | ✓ | Default PHONE; WEB / WALK_IN / APP |
occasion | text | Birthday / anniversary / etc. | |
status | text | ✓ | PENDING (default) / CONFIRMED / CHECKED_IN / CANCELLED |
confirmedAt / checkedInAt / cancelledAt | timestamptz | — | |
cancellationReason | text | — | |
saleOrderId | text | FK after check-in |
3.7 PosSession / PosSessionReport
PosSession:
| Field | Type | Required | Description |
|---|---|---|---|
merchantId | text | ✓ | Owner |
saleChannelId | text | ✓ | FK |
deviceId | text | ✓ | FK |
openedById | text | ✓ | User |
closedById | text | — | |
status | text | ✓ | OPEN (default) / CLOSED |
openedAt / closedAt | timestamptz | — | |
openingFloat | decimal(15,4) | ✓ | Default 0 |
expectedCash / actualCash / cashDiscrepancy | decimal(15,4) | Reconciliation | |
expectedNonCash / actualNonCash | jsonb | TPosSessionNonCashBreakdown | |
closeRecountCount | int | ✓ | Default 0; tracks recount attempts |
notes | text | — |
PosSessionReport: snapshot of session metrics on close (sales, refunds, cash flow).
3.8 Customer
| Field | Type | Required | Description |
|---|---|---|---|
name | text | ✓ | — |
phone | text | — | |
email | text | — | |
userId | text | Linked user account (optional) | |
merchantId | text | ✓ | Owner |
pointBalance | decimal(15,4) | ✓ | Default 0 |
3.9 PointTransaction
| Field | Type | Required | Description |
|---|---|---|---|
customerId | text | ✓ | FK |
merchantId | text | ✓ | Owner |
saleOrderId | text | ✓ | Source order |
type | text | ✓ | AWARD / REDEEM / ADJUST (per PointTransactionTypes) |
points | decimal(15,4) | ✓ | Signed delta |
conversionRate | decimal(15,4) | ✓ | Snapshot of points-per-currency at award time |
Idempotency:
PointTransactionRepository.existsBySaleOrderIdblocks duplicate awards.
4. Status Enums
4.1 SaleOrderStatuses
| Value | Stage |
|---|---|
DRAFT | Cart / mutable items |
PROCESSING | Checkout complete, awaiting payment |
PARTIAL | Some payment received |
COMPLETED | Fully paid |
CANCELLED | Terminal |
4.2 SaleCheckStatuses
| Value | Stage |
|---|---|
PROCESSING | Default — accepting payments |
PARTIAL | Some payment received |
COMPLETED | Fully paid |
CANCELLED | Terminal |
4.3 KitchenTicketStatuses
Source:
core/src/models/schemas/sale/kitchen-ticket/constants.ts. The ticket is at a different layer than the items — they have different status sets.
| Value | Code | Stage |
|---|---|---|
PENDING | 103_PENDING | Just sent to kitchen, no item COOKING yet |
PROCESSING | 203_PROCESSING | At least one item is COOKING (auto-progress from PENDING) |
READY | 302_SUCCESS | All items READY-or-beyond (auto) |
COMPLETED | 303_COMPLETED | All items terminal, ≥1 SERVED (auto) |
VOIDED | 505_CANCELLED | Manually voided |
Helper guards: canVoid (any active), canProgress (PENDING only), canMarkReady (PROCESSING only), canComplete (READY only).
4.4 KitchenTicketItemStatuses
Source:
core/src/models/schemas/sale/kitchen-ticket-item/constants.ts. Different set from the ticket-level enum.
| Value | Code | Trigger |
|---|---|---|
PENDING | 103_PENDING | Initial |
COOKING | 203_PROCESSING | startCookingItem |
READY | 302_SUCCESS | markItemReady — emits Kafka KITCHEN_TICKET_ITEM_STATUS_CHANGED |
SERVED | 303_COMPLETED | markItemServed |
VOIDED | 505_CANCELLED | voidTicketItem |
4.5 AllocationUsageStatuses
| Value | Stage |
|---|---|
ACTIVE | Reserved/occupied |
SUCCESS | Order paid → usage closed |
CANCELLED | Order/reservation cancelled |
EXPIRED | Reservation timeout |
4.6 ReservationStatuses
| Value | Stage |
|---|---|
PENDING | Created, awaiting confirmation |
CONFIRMED | Confirmed by host |
CHECKED_IN | Guest arrived; spawns SaleOrder |
CANCELLED | Terminal |
4.7 PosSessionStatuses
| Value | Stage |
|---|---|
OPEN | Active shift |
CLOSED | Reconciled and closed |
5. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
SaleOrder.subtotal/tax/discount/total = Σ(items) | Service updateSummaryFromItems after every item mutation |
At most one OPEN PosSession per (merchantId, deviceId) | Service validateAndAttachSession |
SaleCheck totals = Σ(SaleCheckItem) for its lines | SaleCheckRepository.recalculateTotals |
KitchenTicket auto-progresses through PENDING→COOKING→READY→SERVED based on item statuses | KitchenTicketRepository.evaluateTicketAutoProgression after every item status change |
KitchenTicket.ticketNumber unique partial per kitchen station (sequence reset on station change) | Schema partial unique + getNextSequence |
PointTransaction idempotent per (customerId, saleOrderId) | existsBySaleOrderId lookup before write |
AllocationUsage follows order/reservation lifecycle (cancel cascades) | Service-level on cancelOrder / cancellation |
Order merge / split operations preserve item totals | Service-level transaction; transferHistory audit |
6. Soft-delete Behavior
| Entity | Soft-delete | Notes |
|---|---|---|
| All sale entities | ✓ | deletedAt marker; archive for SaleOrder = soft-delete |
KitchenTicket voidedAt | logical | voidedAt is not a soft-delete; ticket remains queryable |