Skip to content

URD: Pricing

ModuleVersionUpdated
CORE-14v0.42026-07-15

What the Pricing module does

Define user-facing requirements for the Pricing engine - how a merchant sets prices, how taxes are computed, how variant costs are tracked, and how promotions are configured. The outcome is that every product line and every order is priced consistently and auditably, both when an owner configures it and when the sale flow asks for a price at checkout.

Five points to understand first

This is the most important part of the document. Understanding the five points below is understanding 80% of the module.

1. Picking a fare is a single-elimination contest run over three rounds

A variant can carry several fares at once: a base price, a seasonal fare, a loyal-customer fare. The engine doesn't ask itself "which fare looks best" - it runs exactly three elimination rounds, in a fixed order that never changes.

RoundWhat happens
1. Filter by tierAmong every valid fare (one that has passed all of its rule conditions) plus the base price, only the highest-priority tier survives. Fares in a lower tier are eliminated outright, no matter how cheap they are.
2. OVERRIDE takes priorityWithin the surviving tier, if at least one OVERRIDE fare is present, it wins outright - no price comparison against any SALE fare in the same tier. With several OVERRIDEs, the cheapest one among them wins.
3. Cheapest in the tierWith no OVERRIDE in the tier, the cheapest fare in that tier wins - it may be a SALE fare, or it may be the base price itself if the base price landed in this tier.

The base price isn't a bystander in the contest. It competes at its own priority level, defaulting to 0. That means a child fare sitting at priority 0 doesn't automatically beat the base price either - the two still have to compete on price like any other pair.

A worked example - round 2 decides it:

A variant has a base price of 100,000 VND (priority 0). The owner adds a child SALE fare at 80,000 VND and a child OVERRIDE fare at 90,000 VND, both set to priority 1 and both currently valid (they've passed their own rule conditions).

  1. Filter by tier: the highest tier among the valid candidates is priority 1. The base price sits at priority 0, so it's eliminated from this round right away, even though it's still a valid option in principle.
  2. Tier 1 has one OVERRIDE (90,000 VND) that's valid.
  3. The OVERRIDE wins immediately - the engine doesn't bother comparing 90,000 VND against the SALE fare's 80,000 VND. The customer pays 90,000 VND, even though the SALE fare is 10,000 VND cheaper.

The design reason: OVERRIDE is the owner's hard override, a deliberate act of will - a contract price, a locked-in combo price, a price no one gets to haggle further on. SALE is just one discount competing against other discounts. When both are present in the same tier, override always beats competition.

A second example - the rule condition decides whether a fare even enters the contest:

Same variant, base price 100,000 VND. This time there's only one child SALE fare at 80,000 VND, carrying the rule "quantity purchased is 10 or more".

  • A customer buys 12 units: the quantity rule passes, the SALE fare is valid. The highest tier among valid candidates now holds both the base price and the SALE fare (if they share a priority level) - no OVERRIDE in the tier, so the cheapest fare wins: 80,000 VND.
  • A customer buys 5 units: the quantity rule fails, the SALE fare is eliminated right at the rule filter, as if it never entered the contest. With no valid child fare left, the engine falls straight back to the base price: 100,000 VND.

When no child fare is valid, the engine doesn't go looking for any other option - it falls straight back to the base price, with no error and no blocked transaction.

2. A pricing snapshot is immutable - every figure is copied, never referenced back

Every time pricing runs, the result doesn't store a link back to the fare or tax configuration it used. It copies everything outright: the display name, the base value, the amount, who bears that charge, whether it's embedded in the price - all frozen at the moment of calculation.

So if the owner later renames a tax type, or turns off a fare that was used to sell something yesterday, yesterday's order still displays exactly as it did at the time of sale: the correct tax name, the correct amount, the correct fare applied. The snapshot keeps only a reference code back to the source record for lookup purposes, but every figure shown is read from the frozen copy, never recalculated from the current configuration.

This is mandatory for invoicing, reconciliation, and dispute resolution - an invoice is not allowed to change its numbers over time.

3. Tax: inclusive and exclusive are two opposite ways of computing the same charge

A tax can sit in one of two ways:

  • Exclusive: the tax is computed on the price and then added on top. The total rises by exactly the tax amount.
  • Inclusive: the tax is already embedded in the displayed price, and the engine works it out backward to know how much of that price is tax. The total the customer pays doesn't change.

A worked example: a line priced at 110,000 VND, with a 10% tax rate.

  • If this tax is exclusive: tax = 110,000 x 10% = 11,000 VND, the customer pays a total of 121,000 VND.
  • If this tax is inclusive: tax = 110,000 - 110,000 / 1.1 = 10,000 VND, the customer still pays only 110,000 VND as listed - the tax was already inside that figure, and it's only broken out to know how much of it is tax.

A line can carry several taxes stacked in priority order, and a tax can be compound - computed on the running total that already includes the taxes ahead of it, instead of on the original base price. For example, two exclusive taxes stacked in order: 10% then 5% compound, on a base of 100,000 VND. The first tax is computed on 100,000, giving 10,000, which pushes the running total to 110,000. The second, compound tax is computed on 110,000 (not the original 100,000), giving 5,500. Total tax is 15,500, not 15,000 as it would be if each tax were computed separately on the base.

A line with no tax attached at all still prices normally - the tax portion is simply zero, nothing is blocked for missing configuration.

4. A per-party ledger - every amount must record who bears it

Every amount inside a priced line (price, tax, discount, fee) is attached to a specific bearing party - today two parties are wired in, buyer and seller, while the marketplace platform, upstream vendor, and tax authority are reserved for charges not yet turned on.

Most taxes are borne by the buyer, either added on top or already embedded in what the customer pays. But the owner can also configure a tax the merchant itself bears - that charge is never added to the invoice the customer receives; it's recorded separately in the seller's share of the ledger.

Example: a line priced at 100,000 VND, with a 10% exclusive VAT borne by the buyer = 10,000 VND. The customer pays a total of 110,000 VND. If that same line also carries a 2,000 VND charge the merchant has to absorb itself (not counted into the price charged to the customer), that charge doesn't add to the 110,000 VND the customer pays - it only shows up in the seller's share of the ledger, so the owner knows exactly what's left after that obligation is deducted.

Both the line snapshot and the order snapshot roll up amounts split exactly this way by party - what the buyer owes in one column, what each party bears in another - so reading a report never requires re-adding everything from scratch.

5. Cost is a completely separate axis from price

A fare decides what the customer pays. Cost answers a completely different question: what did the merchant spend to acquire one unit of that item. The two figures have no bearing on each other inside the fare-selection engine - changing the cost doesn't change the price, and vice versa.

Cost is tracked over effective date ranges, not as one static figure. At any given moment, a variant has exactly one open (not-yet-ended) cost record - that's the current cost. When the owner updates the cost, the old record is closed (stamped with an end date), and a new record opens to replace it, rather than overwriting the old one.

Example: a variant's current cost is 50,000 VND, with no end date on that record. On July 10, the owner updates the cost to 60,000 VND. The 50,000 VND record is closed with an end date of July 10; a new 60,000 VND record opens with no end date, becoming the current cost. The full cost history is preserved, which margin calculation and reporting rely on over time - not just knowing the cost "right now", but being able to look back at what the cost was "back then".

What is in the module

Five feature areas. Click an AREA code to jump to the Details tab and read its requirements.

AREANameStatus
FAREFares & Fare SetsBuilt
TAXTax ComputationBuilt
COSTCost TrackingBuilt
SIMPricing Simulation & PreviewBuilt
PROMOPromotions & RulesPartial

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