Domain Model
All schemas are defined in
@nx/core/src/models/schemas/{public,allocation}/and re-exported. Catalog tables live in thepublicschema; allocation tables in theallocationschema. Polymorphic columns usegeneratePrincipalColumnDefs; common columns viagenerateCommonColumnDefs<TMetadata>().
1. Full ERD
2. Common Columns
| Column | Type | Notes |
|---|---|---|
id | text | PK, Snowflake |
identifier | text | Human code with prefix (P, PV, M, …) |
createdAt / modifiedAt | timestamptz | — |
createdBy / modifiedBy | text | User audit |
deletedAt | timestamptz | Soft-delete |
metadata | jsonb | Typed extension bag ($type<TMetadata>()) |
3. Entities
3.1 Product
| Property | Value |
|---|---|
| Table | Product (public) |
| Source | core/.../public/product/schema.ts |
| Soft-delete | yes |
| Field | Type | Required | Description |
|---|---|---|---|
slug | text | ✓ | Unique partial per merchantId |
status | text | ✓ | ProductStatuses; default ACTIVATED |
merchantId | text | ✓ | Owner |
taxGroupId | text | Tax group ref (consumed by taxation) | |
parentId | text | Hierarchy / variant-group | |
referenceId | text | External / sync reference |
3.2 ProductVariant
| Property | Value |
|---|---|
| Table | ProductVariant (public) |
| Source | core/.../public/product-variant/schema.ts |
| Field | Type | Required | Description |
|---|---|---|---|
slug | text | ✓ | — |
isDefault | boolean | ✓ | Default variant of the product (default false) |
dateFrom / dateTo | timestamptz | Validity window | |
status | text | ✓ | ProductVariantStatuses; default ACTIVATED |
type | text | ✓ | ProductVariantTypes; default STORABLE — structural discriminator |
uom | jsonb | IUomRole { base, purchase, sale } | |
productId | text | ✓ | Parent product |
typedrives stocking & bundling — see §4.1. Pricing is not stored here (lives in@nx/pricing).
3.3 ProductInfo / ProductIdentifier
ProductInfo (polymorphic generatePrincipalColumnDefs — principal = Product or ProductVariant): name (i18n), description (i18n).
ProductIdentifier (polymorphic): scheme (TProductIdentifierScheme — SYSTEM / SKU / BARCODE / …), identifier (text). SYSTEM scheme is auto-generated on aggregate create.
3.4 ProductOption / ProductOptionValue / ProductVariantOption
| Table | Key fields |
|---|---|
ProductOption | productId, key, name (i18n), sequence |
ProductOptionValue | optionId, value, name (i18n), sequence |
ProductVariantOption | productVariantId, optionId, optionValueId — links a variant to one value per option |
3.5 ProductBundler
| Property | Value |
|---|---|
| Table | ProductBundler (public) |
| Source | core/.../public/product-bundler/{schema,constants}.ts |
| Field | Type | Required | Description |
|---|---|---|---|
type | text | ✓ | ProductBundlerTypes COMBO / ADDON / FBT; default COMBO |
leadVariantId | text | ✓ | Host / combo variant |
relatedVariantId | text | ✓ | Component / addon / suggestion |
quantity | numeric | ✓ | Default 1 |
basis | text | ProductBundlerBasises — FBT price override; null on COMBO/ADDON | |
basisValue | numeric | Override amount/percent | |
sequence | int | ✓ | Ordering (default 0) |
Unique partial on
(type, leadVariantId, relatedVariantId). This single table expresses COMBO/ADDON/FBT relations — they are not variant types. See ADR-0003.
3.6 Merchant
| Property | Value |
|---|---|
| Table | Merchant (public) |
| Source | core/.../public/merchant/{schema,constants}.ts |
| Field | Type | Required | Description |
|---|---|---|---|
slug | text | ✓ | Unique partial per organizerId |
name / description | i18n | name ✓ | — |
status | text | ✓ | MerchantStatuses; default ACTIVATED |
currency | text | ✓ | Default VND |
businessType | text | ✓ | BusinessTypes; default HOUSEHOLD |
industry | text | ✓ | MerchantIndustry; default FNB |
isHeadquarter | boolean | ✓ | Default false |
location | jsonb | location() helper | |
taxMethod | text | TaxMethods (e.g. DIRECT) | |
parentId | text | Hierarchy | |
organizerId | text | ✓ | Owner |
metadata.tax | jsonb | { taxCode, cityCode?, districtCode?, wardsCode?, fullName?, addressLine? } — MST capture (CDC → TaxInfo) | |
metadata.eInvoice[] | jsonb | e-invoice provider creds | |
metadata.finance.accounts[] | jsonb | seeded finance accounts | |
metadata.onboarding | jsonb | Partial<Record<TMerchantOnboardingStep, boolean>> |
3.7 Organizer
slug (unique), identifier, status (OrganizerStatuses; default ACTIVATED), name (i18n), description (i18n), location, parentId, headquarterMerchantId.
3.8 SaleChannel / SaleChannelProduct
SaleChannel: slug, identifier, status (default ACTIVATED), name (i18n), description (i18n), merchantId, parentId.
SaleChannelProduct (join): productId, saleChannelId. Cascade-deleted by DeletionPolicyService when a channel is removed.
3.9 Category
| Field | Type | Required | Description |
|---|---|---|---|
name / description | i18n | name ✓ | — |
status | text | CategoryStatuses | |
discriminationType | text | TCategoryDiscriminationType (FE grouping label) | |
identifier | text | ✓ | — |
merchantId | text | null ⇒ SYSTEM category (shared by businessType) | |
parentId | text | Self-ref hierarchy |
Category.typeis FE-grouping only — it does not drive inventory/sale behavior.
3.10 Configuration
| Field | Type | Required | Description |
|---|---|---|---|
principalType / principalId | text | Owner scope (null ⇒ SYSTEM) | |
code | text | ✓ | Config code; provider-integration format {type}:{provider}:{action}:{credentialType} |
group | text | ✓ | ConfigurationGroups |
status | text | ✓ | ConfigurationStatuses |
environment | text | Per-environment scoping | |
credential | text | AES-256-GCM ciphertext (masked in responses) |
Unique partial on
(group, code, principalId, principalType, environment).
3.11 Device / DiscriminationType / ReceiptTemplate / Setting
| Table | Key fields |
|---|---|
Device | status (default NEW), name (i18n), code, type, merchantId, hardwareInfo/softwareInfo (jsonb), pin, vendor |
DiscriminationType | scope, status, name (i18n), type, parentId, merchantId |
ReceiptTemplate | polymorphic principal, name, locale, paperWidth, fontSize, isDefault, content (jsonb TReceiptContent) |
Setting | polymorphic principal, status, typed metadata value bag |
3.12 Allocation (allocation schema)
| Table | Key fields |
|---|---|
AllocationLayout | top-level floor plan |
AllocationZone | name (i18n), layoutId, parentId (self-ref), style (jsonb) |
AllocationUnit | name (i18n), zoneId, placement/style (jsonb), capacity, status |
AllocationUsage(occupancy) is owned and mutated by@nx/sale; commerce owns only the static layout.
4. Status & Type Enums
4.1 ProductVariantTypes
Source:
core/.../public/product-variant/constants.ts.
| Value | Const | Stockable | BOM |
|---|---|---|---|
000_STORABLE | STORABLE | ✓ | — |
100_CONSUMABLE | CONSUMABLE | — | — |
200_SERVICE | SERVICE | — | — |
300_KIT | KIT | — | ✓ (via Material) |
301_COMBO | COMBO | — | ✓ (via ProductVariant) |
400_MANUFACTURED | MANUFACTURED | ✓ | ✓ |
Helper sets: STOCKABLE_SET = {STORABLE, MANUFACTURED}, BOMABLE_SET = {KIT, COMBO, MANUFACTURED}.
4.2 ProductBundlerTypes
| Value | Const | Meaning |
|---|---|---|
000_COMBO | COMBO | relatedVariantId is a component of the combo |
100_ADDON | ADDON | free addon attached to host variant |
200_FBT | FBT | "frequently bought together" suggestion (directional) |
4.3 Lifecycle statuses
| Enum | Values |
|---|---|
ProductStatuses / ProductVariantStatuses | DRAFT, ACTIVATED, DEACTIVATED, ARCHIVED |
MerchantStatuses / OrganizerStatuses / SaleChannelStatuses | reuse Statuses incl. ACTIVATED (default) |
MerchantOnboardingSteps | BUSINESS, FINANCE_ACCOUNT (300), TAX_INFO (400), PRODUCT |
5. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
| Product aggregate is atomic (Product + Info + SYSTEM identifier + channel links + default variant) | Single TX in ProductCreateService |
| Merchant aggregate atomic (merchant + policy + identifier + categories + channels) | Single TX in MerchantService |
| Onboarding creates Organizer + Merchant + 2 PolicyDefinitions + default SaleChannel atomically | OrganizerService.onBoarding TX |
Product.slug unique per merchantId; Merchant.slug unique per organizerId | DB unique partial (deletedAt IS NULL) |
Multi-merchant product replication runs only when syncMerchantIds.length > 0 | ProductAggregate*Listener.pushJobToQueue guard |
Merchant tax info lives in metadata.tax, not a column; TaxInfo is authoritative downstream | MerchantService metadata merge → CDC |
| Provider credentials never returned in plaintext | EncryptService + masked display value |
Configuration uniqueness | (group, code, principalId, principalType, environment) unique partial |
ProductBundler row unique per (type, leadVariantId, relatedVariantId) | DB unique partial |
6. Soft-delete Behavior
| Entity | Soft-delete | Notes |
|---|---|---|
| All commerce entities | ✓ | deletedAt marker; unique partials exclude soft-deleted rows |
| Deletion guards | — | DeletionPolicyService blocks delete when sale history exists / strict policy set; archive instead |