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
shiftIdtextFK to Shift
openedInShiftEnrollmentId / closedInShiftEnrollmentIdtextFK to ShiftEnrollment
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 Shift / ShiftDrawer / ShiftEnrollment / ShiftEnrollmentEvent / ShiftReport

Shift v2 model. Source: core/src/models/schemas/sale/shift*. All entities soft-deletable.

Shift - top-level shift aggregate, scoped to (merchantId, saleChannelId):

FieldTypeRequiredDescription
merchantIdtextOwner
saleChannelIdtextChannel scope
codetextIdentifier (prefix SHIFT)
namejsonbi18n { default, en?, vi? }
notesjsonbi18n { default, en?, vi? }
statustextSee §4.7; default 100_OPENING
openedByIdtextUser who opened
closedByIdtextUser who closed
openedAt / closedAttimestamptzopenedAt defaults to now
startAt / endAttimestamptzPlanned window (optional)
metadatajsonbFree-form (legacy openNotes/closeNotes removed - notes live in the notes column)

A channel may have multiple 100_OPENING shifts at once (per-device single-open is enforced via enrollment); (merchantId, saleChannelId) is a plain composite index.

ShiftDrawer - per-device cash drawer, 1:1 with a ShiftEnrollment:

FieldTypeRequiredDescription
shiftEnrollmentIdtext1:1 owner (partial unique)
shiftId / merchantId / deviceIdtextScope
statustextSame set as Shift; default 100_OPENING
openingFloatdecimal(15,4)Cash float at open
expectedCash / actualCash / cashDiscrepancydecimal(15,4)Reconciliation
expectedNonCash / actualNonCashjsonbTShiftNonCashBreakdown (per non-cash method)
closeRecountCountintDefault 0; recount attempts
openedAt / closedAttimestamptzopenedAt defaults to now

ShiftEnrollment - a principal (device or user) joined to a Shift:

FieldTypeRequiredDescription
shiftId / merchantIdtextScope
principalTypetextDevice / User
principalIdtextDevice or user id
statustextACTIVATED (default) / DEACTIVATED
roletextEnrollment role
drawerIdtextAttached shared-drawer session
enrolledByIdtext-
enrolledAt / leftAttimestamptzenrolledAt defaults to now
notesjsonbi18n { default, en?, vi? } - note supplied at join/create

At most one ACTIVATED enrollment per principal - partial unique index.

ShiftEnrollmentEvent - append-only ledger of enrollment + cash-movement events:

FieldTypeRequiredDescription
shiftEnrollmentId / shiftId / merchantIdtextScope
typetextSee §4.8
amountdecimal(15,4)Set only for cash movements (PAY_IN / PAY_OUT / SAFE_DROP)
occurredAttimestamptzDefaults to now
notetext-

ShiftReport - X/Z report rows; per-enrollment when shiftEnrollmentId is set, shift-wide when NULL:

FieldTypeRequiredDescription
shiftId / merchantId / saleChannelIdtextScope
shiftEnrollmentIdtextNULL = shift-wide aggregate; set = per-enrollment
reportTypetextX (live snapshot) / Z (final)
reportNointSequential per scope
openedById / closedById / generatedByIdtextAudit
openedAt / closedAt / generatedAttimestamptzgeneratedAt defaults to now
openingFloat, cashSales, cashRefunds, payIns, payOuts, safeDrops, expectedCash, actualCash, cashDiscrepancydecimal(15,4)Cash summary
grossSales, discounts, taxAmount, netSales, voidAmountdecimal(15,4)Sales summary
orderCount / itemCount / voidCountintDefault 0
paymentMethods / sections / categoryBreakdown / actualNonCashjsonbBreakdowns
notestext-

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 ShiftStatuses

Source: core/src/models/schemas/sale/shift/constants.ts. ShiftDrawerStatuses uses the identical set; isOpen = 100_OPENING.

ValueStage
100_OPENINGActive shift / drawer
200_CLOSEDClosed, awaiting reconciliation
300_RECONCILEDCash reconciled - terminal

4.8 ShiftEnrollment Enums

ShiftEnrollmentStatuses: ACTIVATED (default) ↔ DEACTIVATED.

ShiftPrincipalType: Device / User.

ShiftEnrollmentEventTypes - append-only ledger; amount is set only for cash movements:

ValueCash movement
DROP_IN / DROP_OUTno
CHECK_IN / CHECK_OUTno
DRAWER_OPEN / RECONCILEno
PAY_IN / PAY_OUT / SAFE_DROPyes (amount required)

4.9 ShiftReportTypes

ValueStage
XLive snapshot (re-runnable)
ZFinal report - one per scope

5. Cross-entity Invariants

InvariantEnforcement
SaleOrder.subtotal/tax/discount/total = Σ(items)Service updateSummaryFromItems after every item mutation
At most one 100_OPENING Shift per (merchantId, saleChannelId)Partial unique index + ShiftService
At most one ACTIVATED ShiftEnrollment per principalPartial unique index
ShiftDrawer is 1:1 with a ShiftEnrollmentPartial unique index on shiftEnrollmentId
ShiftEnrollmentEvent.amount set only for PAY_IN / PAY_OUT / SAFE_DROPShiftEnrollmentEventTypes.isCashMovement
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.