Skip to content

Domain Model

Sale tables live in sale PostgreSQL schema; allocation tables in allocation schema. All schemas defined in @nx/core/src/models/schemas/{sale,allocation}/. Numeric columns use decimal(15, 4).

1. Full ERD

2. Common Columns

ColumnTypeNotes
idtextPK, Snowflake
createdAt / modifiedAttimestamptz
createdBy / modifiedBytextUser audit
deletedAttimestamptzSoft-delete
metadatajsonbExtension bag

3. Entities

3.1 SaleOrder

PropertyValue
TableSaleOrder
Sourcecore/src/models/schemas/sale/sale-order/schema.ts
Soft-deleteyes
FieldTypeRequiredDescription
orderNumbertextUnique partial per merchant
name / slugtextSlug unique partial
validityjsonb{ from, to } for time-limited orders
statustextSee §4.1; default DRAFT
draftAt / processingAt / partialAt / completedAt / cancelledAttimestamptzPer-status timestamps
cancellationReasontext
customerIdtextFK
merchantIdtextOwner
saleChannelIdtextFK
openedInSessionId / closedInSessionIdtextFK to PosSession
currencytextDefault VND
exchangeRatedecimal(12,6)Default 1
subtotal / tax / discount / totaldecimal(15,4)Default 0; maintained by updateSummaryFromItems
originOrderIdtextFor order-split parent tracking
checkSplitAt / orderSplitAt / mergedAttimestamptzOperation timestamps
counterjsonb{ paid, paidItemIds[], total } — payment progress

3.2 SaleOrderItem

PropertyValue
TableSaleOrderItem
Polymorphic(itemType, itemId) via generatePrincipalColumnDefs({ discriminator: 'item', defaultPolymorphic: 'ProductVariant' })
FieldTypeRequiredDescription
saleOrderIdtextFK
itemTypetextPRODUCT_VARIANT (default) / other
itemIdtextFK target
modetextPRODUCT (default — auto-merge duplicates) / CUSTOM (always new line)
leadItemIdtextGroup lead for combo items
currencytextDefault VND
basePrice / unitPricedecimal(15,4)Pre-discount / post-discount per-unit
discount / taxdecimal(15,4)Default 0
quantitydecimal(15,4)Default 1
totaldecimal(15,4)Computed
fareId / fareProvidertextPricing source ref
priceMetadatajsonbPricing snapshot (v2 detail)
transferHistoryjsonbArray<TTransferHistoryEntry> — merge/split tracking
recipeIdtextLinked active MaterialRecipe.id (snapshot)

3.3 SaleCheck / SaleCheckItem

SaleCheck:

FieldTypeRequiredDescription
saleOrderIdtextParent order
statustextPROCESSING (default) / COMPLETED / CANCELLED
subtotal / tax / discount / totaldecimal(15,4)Recalculated by recalculateTotals
customerIdtextPer-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:

FieldTypeRequiredDescription
ticketNumbertextUnique partial; sequential per station
saleOrderIdtextFK
merchantIdtextOwner
kitchenStationIdtextRouting target (optional)
statustextSee §4.3; default PENDING
priorityintDefault 0 (rush flag bumps priority)
sequenceintDefault 1 (ordering hint)
pendingAt / processingAt / readyAt / completedAt / voidedAttimestamptzStatus timestamps

KitchenTicketItem:

FieldTypeRequiredDescription
kitchenTicketIdtextFK
saleOrderItemIdtextFK
quantitydecimal(15,4)Default 1
statustextSee §4.4; default PENDING
startedAt / readyAt / servedAt / voidedAttimestamptzStatus timestamps

Each status change emits Kafka KITCHEN_TICKET_ITEM_STATUS_CHANGED and triggers ticket auto-progression evaluation.

3.5 AllocationUsage / AllocationUnit / AllocationZone / AllocationLayout

Schema lives in allocation schema (separate from sale).

AllocationUsage — polymorphic usage of an allocation unit:

FieldTypeRequiredDescription
usageTypetextSALE_ORDER / RESERVATION (via discriminator: 'usage')
usageIdtextFK target id
unitIdtextFK to AllocationUnit
merchantIdtextOwner
assigneeIdtextPerson/staff assigned
statustextACTIVE (default) / SUCCESS / CANCELLED / EXPIRED
typetextGENERAL (default) / DINE_IN / TAKEAWAY / DELIVERY
reservedFrom / reservedTo / reservedAt / startedAt / completedAttimestamptzLifecycle 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

FieldTypeRequiredDescription
merchantIdtextOwner
guestNametext
guestPhonetext
guestEmailtext
partySizeint
reservedFromtimestamptz
reservedTotimestamptz
notestext
sourcetextDefault PHONE; WEB / WALK_IN / APP
occasiontextBirthday / anniversary / etc.
statustextPENDING (default) / CONFIRMED / CHECKED_IN / CANCELLED
confirmedAt / checkedInAt / cancelledAttimestamptz
cancellationReasontext
saleOrderIdtextFK after check-in

3.7 PosSession / PosSessionReport

PosSession:

FieldTypeRequiredDescription
merchantIdtextOwner
saleChannelIdtextFK
deviceIdtextFK
openedByIdtextUser
closedByIdtext
statustextOPEN (default) / CLOSED
openedAt / closedAttimestamptz
openingFloatdecimal(15,4)Default 0
expectedCash / actualCash / cashDiscrepancydecimal(15,4)Reconciliation
expectedNonCash / actualNonCashjsonbTPosSessionNonCashBreakdown
closeRecountCountintDefault 0; tracks recount attempts
notestext

PosSessionReport: snapshot of session metrics on close (sales, refunds, cash flow).

3.8 Customer

FieldTypeRequiredDescription
nametext
phonetext
emailtext
userIdtextLinked user account (optional)
merchantIdtextOwner
pointBalancedecimal(15,4)Default 0

3.9 PointTransaction

FieldTypeRequiredDescription
customerIdtextFK
merchantIdtextOwner
saleOrderIdtextSource order
typetextAWARD / REDEEM / ADJUST (per PointTransactionTypes)
pointsdecimal(15,4)Signed delta
conversionRatedecimal(15,4)Snapshot of points-per-currency at award time

Idempotency: PointTransactionRepository.existsBySaleOrderId blocks duplicate awards.

4. Status Enums

4.1 SaleOrderStatuses

ValueStage
DRAFTCart / mutable items
PROCESSINGCheckout complete, awaiting payment
PARTIALSome payment received
COMPLETEDFully paid
CANCELLEDTerminal

4.2 SaleCheckStatuses

ValueStage
PROCESSINGDefault — accepting payments
PARTIALSome payment received
COMPLETEDFully paid
CANCELLEDTerminal

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.

ValueCodeStage
PENDING103_PENDINGJust sent to kitchen, no item COOKING yet
PROCESSING203_PROCESSINGAt least one item is COOKING (auto-progress from PENDING)
READY302_SUCCESSAll items READY-or-beyond (auto)
COMPLETED303_COMPLETEDAll items terminal, ≥1 SERVED (auto)
VOIDED505_CANCELLEDManually 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.

ValueCodeTrigger
PENDING103_PENDINGInitial
COOKING203_PROCESSINGstartCookingItem
READY302_SUCCESSmarkItemReady — emits Kafka KITCHEN_TICKET_ITEM_STATUS_CHANGED
SERVED303_COMPLETEDmarkItemServed
VOIDED505_CANCELLEDvoidTicketItem

4.5 AllocationUsageStatuses

ValueStage
ACTIVEReserved/occupied
SUCCESSOrder paid → usage closed
CANCELLEDOrder/reservation cancelled
EXPIREDReservation timeout

4.6 ReservationStatuses

ValueStage
PENDINGCreated, awaiting confirmation
CONFIRMEDConfirmed by host
CHECKED_INGuest arrived; spawns SaleOrder
CANCELLEDTerminal

4.7 PosSessionStatuses

ValueStage
OPENActive shift
CLOSEDReconciled and closed

5. Cross-entity Invariants

InvariantEnforcement
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 linesSaleCheckRepository.recalculateTotals
KitchenTicket auto-progresses through PENDING→COOKING→READY→SERVED based on item statusesKitchenTicketRepository.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 totalsService-level transaction; transferHistory audit

6. Soft-delete Behavior

EntitySoft-deleteNotes
All sale entitiesdeletedAt marker; archive for SaleOrder = soft-delete
KitchenTicket voidedAtlogicalvoidedAt is not a soft-delete; ticket remains queryable

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