Domain Model
Pricing owns no schema. All tables below are defined in
@nx/coreunderpackages/core/src/models/schemas/pricing/, in the PostgreSQLpricingschema. Pricing re-exports them as repositories.
1. Full ERD
2. Entities
One block per table. Schema source:
packages/core/src/models/schemas/pricing/.
FareSet
| Property | Value |
|---|---|
| Table | pricing.FareSet |
| Source | core/src/models/schemas/pricing/fare-set/schema.ts |
| Soft-delete | yes |
| Owner ID column | productVariantId (soft ref, no FK) |
Fields: id, name (i18n), description (i18n), status (default DEACTIVATED), productVariantId (not null), common columns.
Indexes: IDX on productVariantId, status.
Status enum — FareSetStatuses: ACTIVATED, DEACTIVATED. One ACTIVATED set per variant is the selection invariant.
Fare
| Property | Value |
|---|---|
| Table | pricing.Fare |
| Source | core/src/models/schemas/pricing/fare/schema.ts |
| Soft-delete | yes |
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | text | ✓ | Snowflake | PK |
name / description | i18n jsonb | — | { default, en, vi } | |
status | text | ✓ | ACTIVATED | FareStatuses |
type | text | null | FareTypes.SALE / OVERRIDE / null (child) | |
effectiveFrom / effectiveTo | timestamptz | — | Effective window | |
minQuantity / maxQuantity | decimal(15,4) | — | Quantity gate | |
amount | decimal(15,4) | — | Price (null for OVERRIDE parent group) | |
rulesCount / childrenCount | integer | — | Denormalized counters | |
fareSetId | text | ✓ | — | Owning FareSet |
parentId | text | null | Parent fare (group) |
Indexes: IDX on fareSetId, parentId, status.
Relations: fareSet (M:1), parent (self M:1), children (1:M), fareRules (1:M via polymorphic Rule).
Status enum — FareStatuses: ACTIVATED, DEACTIVATED, ARCHIVED.
Rule
| Property | Value |
|---|---|
| Table | pricing.Rule |
| Source | core/src/models/schemas/pricing/rule/schema.ts |
| Soft-delete | yes |
Fields: polymorphic principalType / principalId, data-type value columns (tValue/nValue/bValue/jValue/boValue + dataType), attribute (not null), priority (not null), operator (not null).
| Concept | Values |
|---|---|
principalType (RulePrincipalTypes) | Fare, Promotion, PromotionMethod |
operator (RuleOperators) | EQ, NE, NEQ, GT, GTE, LT, LTE, IN, INQ, NIN, CONTAINS |
context (RuleContexts, in metadata) | source (buy rule), target (discount rule) |
CONTAINSis the inverse ofIN— context value is an array, rule value scalar. Used by the FBT override (orderProductVariantIds CONTAINS leadVariantId).
TaxSet
| Property | Value |
|---|---|
| Table | pricing.TaxSet |
| Source | core/src/models/schemas/pricing/tax-set/schema.ts |
| Soft-delete | yes |
Fields: polymorphic principalType / principalId, name (i18n), status (default ACTIVATED), sourceType, sourceId.
Indexes: unique-partial on (principalId, principalType, status) where deletedAt IS NULL; IDX on sourceId, (sourceType, sourceId), status.
principalType='ProductVariant'→ item-level taxes;principalType='Merchant'→ order-level taxes.
Tax
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | i18n jsonb | ✓ | — | — |
type | text | 100_PERCENTAGE | TaxModes: AMOUNT / PERCENTAGE / PER_UNIT_AMOUNT | |
value | decimal(15,4) | ✓ | — | Rate or fixed amount |
effectiveFrom | timestamptz | ✓ | — | — |
effectiveTo | timestamptz | — | — | |
priority | integer | 0 | Ascending; same priority share a base | |
isInclusive | boolean | ✓ | false | Embedded in displayed price (back-calc) |
shouldApplyOnDiscounted | boolean | ✓ | true | Discount-aware basis |
isCompound | boolean | ✓ | true | Compounds on prior taxes |
minQuantity / maxQuantity | decimal(15,4) | — | Quantity condition | |
usage | text | ✓ | 000_SALE | TaxUsages.SALE / PURCHASE |
chargeTarget | text | ✓ | 000_CUSTOMER | CUSTOMER (on invoice) / MERCHANT (PIT) |
taxSetId | text | ✓ | — | Owning TaxSet |
taxTypeId | text | — | Classification | |
discriminationTypeId | text | ✓ | — | VN tax classification ref |
Indexes: IDX on discriminationTypeId, status, taxSetId, taxTypeId.
TaxType
| Property | Value |
|---|---|
| Table | pricing.TaxType |
| Source | core/src/models/schemas/pricing/tax-type/schema.ts |
Fields: type (FixedTaxTypes: VAT, EXCISE, ENVIRONMENTAL, LUXURY, PIT, CUSTOM), name/description (i18n), chargeTarget, status, merchantId (nullable → system type).
Indexes: unique on (type, merchantId); IDX on merchantId, (merchantId, status).
Cost
| Property | Value |
|---|---|
| Table | pricing.Cost |
| Source | core/src/models/schemas/pricing/cost/schema.ts |
Fields: polymorphic principalType / principalId (getCurrentCost/getEffectiveCost hardcode ProductVariant), user-audit columns, amount (not null), note, effectiveFrom (not null), effectiveTo (nullable = current/open-ended).
Only one open-ended (null
effectiveTo) cost per variant. Not yet wired into either calculator.
Promotion
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
code | text | null | Null = auto-apply only | |
enableStacking | boolean | ✓ | true | — |
isAutomatic | boolean | ✓ | false | — |
isTaxInclusive | boolean | ✓ | false | — |
name | i18n | ✓ | — | — |
effectiveFrom / effectiveTo | timestamptz | — | — | |
type | text | ✓ | STANDARD | PromotionTypes |
status | text | ✓ | DRAFT | PromotionStatuses |
usageLimit / usageCount | integer | usageCount=0 | — | |
rulesCount | integer | ✓ | 0 | Denormalized |
merchantId | text | — | — |
Indexes: unique-partial on code where deletedAt IS NULL; IDX on merchantId, (merchantId, status).
PromotionMethod
Fields: promotionId (not null), type (PromotionMethodType), value (not null), targetType (not null), allocation, currency (default VND), exchangeRate (default 1), maxQuantity, sourceRulesCount/targetRulesCount, BuyGet fields (buyGetTargetQuantity, buyGetSourceMinQuantity).
Indexes: unique-partial on promotionId where deletedAt IS NULL (one method per promotion).
3. Cross-entity Invariants
| Invariant | Enforcement |
|---|---|
Exactly one ACTIVATED FareSet per productVariantId | Worker idempotency + FareSetStatuses.canDeactivate() always false |
| OVERRIDE fare wins over default; else lowest-priced fare | PricingFareCalculatorService / FareCalculatorService selection |
| Child fares evaluated only when their AND-rules all pass | PricingRuleEvaluatorService (lodash get() attribute path) |
Taxes within the same priority share a base; higher priorities compound | PricingTaxCalculatorService / TaxCalculatorService priority grouping |
| Inclusive tax back-calculated out of displayed price | tax calculator isInclusive branch |
One open-ended Cost (effectiveTo IS NULL) per variant | CostService.updateCurrentCost closes prior before opening new |
| One PromotionMethod per Promotion | unique-partial index on promotionId |
| FBT override seeded at most once per (related FareSet, lead variant) | PricingWorkerService._isFbtOverrideAlreadySeeded |
4. Soft-delete Behavior
| Behavior | Detail |
|---|---|
| Read default | deletedAt IS NULL (model defaultFilter) |
| Hard-delete | Not used by default |
| Unique constraints | Partial — scoped WHERE deletedAt IS NULL (FareSet, TaxSet, Promotion code, PromotionMethod) so a soft-deleted row frees the slot |