URD: Pricing
| Module | CORE-14 | Version | v0.1 |
|---|---|---|---|
| Status | Built | Date | 2026-06-05 |
Business documentation. This URD is Pricing's feature list - each feature below is one Functional Area (
<AREA>). The same<AREA>keys the feature's PRDs (PRD-<AREA>-NNN) and tests (TC-<AREA>-NNN), and each feature is listed in the Delivery feature catalog. See the Feature Spine convention.
1. Purpose
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.
2. Scope
| Included | Excluded |
|---|---|
| Fare sets, fares, parent/child fare hierarchy | Order persistence |
| Rule-based fare selection (quantity, time, channel) | Invoice / e-invoice issuance |
| Tax sets, taxes, tax types | Stock mutation |
| Compound, inclusive/exclusive, priority-ordered tax computation | Payment processing |
| Item-level and order-level taxes | Multi-currency pricing |
| Variant cost tracking with effective date ranges | Discount calculation in checkout (calculator pending) |
| Pricing simulation (v1 flat + v2 snapshot) | Technical API specifications |
| Promotions, promotion methods, eligibility rules |
3. Definitions
| Term | Definition |
|---|---|
| Fare Set | The container of all fares for one product variant. One activated fare set per variant. |
| Fare | A price entry. A default fare is the base price; parent/child fares form a selection group. |
| Default Fare | The fallback base price used when no rule-bearing child fare matches. |
| Parent / Child Fare | A parent fare defines a selection strategy (OVERRIDE or DISCOUNT); child fares carry the conditional prices. |
| Rule | A condition (attribute, operator, value) attached to a child fare or promotion; all rules must pass (AND logic). |
| Tax Set | The container of all taxes for a variant or for a merchant. |
| Tax | A percentage, fixed, or combined charge with priority, temporal window, and inclusive/exclusive/compound flags. |
| Tax Type | A category (e.g. VAT, GST) - system-wide or merchant-scoped. |
| Cost | What a product variant costs the merchant over an effective date range; one is the current cost. |
| Pricing Snapshot | The immutable, auditable v2 result: every applied fare and tax per line, plus order totals. |
| Promotion | A merchant-configured discount campaign with one method and eligibility rules. |
| Promotion Method | The mechanic of a promotion (how the discount is applied). |
4. Conceptual Model
Conceptual only - the full schema lives in the developer domain model.
5. Feature Catalog
The feature list of this module. Each row is one feature (a Functional Area). Detail in §6. Mirrored in the Delivery feature catalog.
| Feature ID | Feature | Phase | Status | Priority |
|---|---|---|---|---|
FARE | Fares & Fare Sets | P1 | Built | High |
TAX | Tax Computation | P1 | Built | High |
COST | Cost Tracking | P2 | Built | High |
SIM | Pricing Simulation & Preview | P2 | Built | High |
PROMO | Promotions & Rules | P3 | In-progress | Medium |
Status: live from Plane where mapped, otherwise registry-declared. Vocabulary mirrors Plane (state-group / phase).
6. Features
One sub-section per feature, in catalog order. Each feature keeps its description, requirements, and acceptance together. Priority = MoSCoW (Must / Should / Could / Won't).
FARE - Fares & Fare Sets Built
Feature ID: pricing/FARE · Phase: P1 · PRDs: Fare & Tax Pricing Engine · Dev: @nx/pricing · Fare System
What it does for users: owners set a base price per variant and, optionally, conditional child prices driven by rules (quantity, time window, channel). The engine picks the winning fare automatically - the first valid child for an OVERRIDE group, or the cheapest valid child for a DISCOUNT group, falling back to the default fare.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-FARE-001 | M | Each product variant has exactly one activated fare set. |
| URD-FARE-002 | M | A fare set + default fare is auto-created when a product variant is created (CDC). |
| URD-FARE-003 | M | Owner can set a default (base) fare per variant. |
| URD-FARE-004 | M | Owner can create a parent fare with one of two strategies: OVERRIDE or DISCOUNT. |
| URD-FARE-005 | M | Owner can add child fares under a parent, each with its own price. |
| URD-FARE-006 | M | Child fares can carry rules (attribute, operator, value) evaluated with AND logic. |
| URD-FARE-007 | M | OVERRIDE: the first valid child fare is selected immediately. |
| URD-FARE-008 | M | DISCOUNT: the lowest-priced valid child fare is selected. |
| URD-FARE-009 | M | When no child fare is valid, the default fare is selected. |
| URD-FARE-010 | S | Fares support an effective-from / effective-to window and quantity bounds. |
| URD-FARE-011 | M | Fares and fare sets are isolated per merchant and use soft-delete. |
Acceptance
AC-FARE-01: Rule-based selection
| Given | When | Then |
|---|---|---|
| Variant with default 100 and a DISCOUNT child of 80 (rule: qty ≥ 10) | Price 12 units | Child fare 80 wins (lowest valid) |
| Same variant | Price 5 units | Default fare 100 wins (no rule matches) |
AC-FARE-02: Auto-seed on variant
| Given | When | Then |
|---|---|---|
| A new product variant | Variant CDC event arrives | A fare set + default fare are auto-created |
TAX - Tax Computation Built
Feature ID: pricing/TAX · Phase: P1 · PRDs: Fare & Tax Pricing Engine · Dev: @nx/pricing · Tax System
What it does for users: owners attach taxes to a variant (item-level) or a merchant (order-level), choosing percentage/fixed amounts, inclusive or exclusive treatment, and compound tax-on-tax. The engine applies them in priority order and returns a clear breakdown.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-TAX-001 | M | Owner can create tax types (e.g. VAT), system-wide or merchant-scoped. |
| URD-TAX-002 | M | Owner can create a tax set and add taxes to it (aggregate update). |
| URD-TAX-003 | M | A tax can be percentage, fixed, or combined (percentage + fixed). |
| URD-TAX-004 | M | Taxes apply in priority order (lower number = higher priority). |
| URD-TAX-005 | M | A tax can be exclusive (added on top) or inclusive (back-calculated from the price). |
| URD-TAX-006 | M | A tax can be compound (computed on the running total including prior taxes). |
| URD-TAX-007 | S | A tax can carry an effective window and min/max quantity conditions. |
| URD-TAX-008 | M | Item-level taxes apply per line; order-level taxes apply to merchant-scoped sets. |
| URD-TAX-009 | M | A default tax rate is applied when no tax set is configured. |
| URD-TAX-010 | M | Tax sets and taxes are isolated per merchant and use soft-delete. |
Acceptance
AC-TAX-01: Inclusive vs exclusive
| Given | When | Then |
|---|---|---|
| Price 110, one 10% exclusive tax | Compute | Tax 11; total 121 |
| Price 110, one 10% inclusive tax | Compute | Tax ≈ 10; total stays 110 (back-calculated) |
AC-TAX-02: Compound order
| Given | When | Then |
|---|---|---|
| Two taxes (10% then 5% compound) on base 100 | Compute | First 10 → base 110 → second 5.5; total tax 15.5 |
COST - Cost Tracking Built
Feature ID: pricing/COST · Phase: P2 · PRDs: - · Dev: @nx/pricing · Cost Tracking
What it does for users: owners record what each variant costs them over time. Updating the current cost ends the prior cost record and opens a new one, so a full cost history is preserved for margin and reporting.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-COST-001 | M | Owner can record a cost for a product variant with an effective date range. |
| URD-COST-002 | M | Owner can read the current cost for a variant. |
| URD-COST-003 | M | Updating the current cost ends the prior record and creates a new one. |
| URD-COST-004 | M | At most one current (open-ended) cost exists per variant at any time. |
| URD-COST-005 | S | Cost history is retained and retrievable. |
| URD-COST-006 | M | Costs are isolated per merchant and use soft-delete. |
Acceptance
AC-COST-01: Update current cost
| Given | When | Then |
|---|---|---|
| Variant with current cost 50 | Owner sets current cost to 60 | Prior 50 record is ended; 60 becomes the current cost |
| Same variant | Read current cost | Returns 60 |
SIM - Pricing Simulation & Preview Built
Feature ID: pricing/SIM · Phase: P2 · PRDs: Pricing simulation & order pricing preview · Dev: @nx/pricing · Simulation
What it does for users: the sale flow prices a whole basket without committing an order. Two stateless endpoints serve it - a v1 flat breakdown (per-line fare, taxes, totals + order totals) for direct display, and a v2 immutable, auditable snapshot (an order snapshot plus one line snapshot each, with a per-party ledger) meant to be persisted on the order. A read-only preview shows a customer their price before checkout; at checkout v1 (authoritative) and v2 (additive, best-effort) run together so the order is priced and its snapshot captured.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-SIM-001 | M | A stateless simulation prices a basket of 1-100 lines and returns per-line and order totals without persisting an order or mutating stock. |
| URD-SIM-002 | M | The v1 simulation (/simulation) returns a flat breakdown - per line: base price, unit price, selected fare + applied fare rules, applied taxes, quantity, subtotal, discount, tax, total; plus order subtotal/discount/tax/total and a compute timestamp. |
| URD-SIM-003 | M | A caller-supplied line id is echoed back and used as the result key, so each priced line maps unambiguously to its request line. |
| URD-SIM-004 | M | Inclusive taxes are embedded in the price and never increase the total; exclusive taxes are added on top - consistent with the tax engine. |
| URD-SIM-005 | M | The v2 simulation (/simulation-v2) returns an immutable, auditable snapshot - an order snapshot plus one line snapshot per line - carrying every applied decision (price, tax, discount, fee). |
| URD-SIM-006 | M | Each v2 applied decision copies its label, base, value, and amount at compute time, so historical rendering stays correct even if the source rule is later renamed or deactivated. |
| URD-SIM-007 | M | A v2 line snapshot exposes ready-to-read totals - buyer-payable plus a per-party (buyer/seller/platform/supplier/government) ledger - and the order snapshot rolls these into subtotal, buyer-payable, per-party ledger, and seller liability. |
| URD-SIM-008 | M | A v2 simulation declares a transaction direction - SALE or PURCHASE - and an order currency (ISO 4217, default VND). |
| URD-SIM-009 | M | The order-pricing preview (POST /sale-orders/{id}/pricing/preview) re-prices an existing order read-only - no persistence, no status change - and returns the v1 breakdown. |
| URD-SIM-010 | M | Pricing an empty cart is refused. |
| URD-SIM-011 | S | At checkout the order is priced with v1 (authoritative) plus v2 (additive, best-effort); a v2 failure is logged and skipped and never blocks checkout. |
| URD-SIM-012 | S | The applied taxes persisted per line merge v1 VAT entries with v2 PIT entries. |
| URD-SIM-013 | M | Each line's pricing context carries quantity, compute time, day-of-week/time/date, the basket's variant ids (for co-occurrence rules), and a derived service window (service time, date, day-of-week, duration), so fare rules can evaluate them. |
| URD-SIM-014 | M | When the upstream pricing service rejects a line (e.g. no active fare set), the sale flow surfaces the real status and reason rather than a generic failure. |
| URD-SIM-015 | M | Both simulation endpoints require authentication and a merchant scope, and all pricing math uses decimal precision. |
| URD-SIM-016 | M | v1 and v2 share the same core fare and tax logic, so a fix lands in both. |
Acceptance
AC-SIM-01: v1 breakdown & totals
| Given | When | Then |
|---|---|---|
| A basket of 2 priceable lines | POST /simulation/calculate | Each line returns base/unit price, selected fare, applied taxes, and line total; the order returns subtotal/discount/tax/total and a compute timestamp |
| One line carries a 10% inclusive tax | Same call | That tax is embedded in the price and does not increase the order total |
AC-SIM-02: Line-id echo & keying
| Given | When | Then |
|---|---|---|
| Items each carrying a caller-supplied line id | Priced via v1 | The result map is keyed by that id and echoes it on each line |
AC-SIM-03: v2 immutable snapshot
| Given | When | Then |
|---|---|---|
| A basket of N lines | POST /simulation-v2/calculate | Returns one order snapshot plus N line snapshots; each line carries a PRICE plus its tax entries, buyer-payable, and a per-party ledger |
| The order snapshot | Same call | Rolls up subtotal, buyer-payable, the per-party ledger, and seller liability |
AC-SIM-04: Preview is read-only
| Given | When | Then |
|---|---|---|
| An existing order with items | POST /sale-orders/{id}/pricing/preview | The v1 breakdown returns and the order's status and stored data are unchanged |
AC-SIM-05: Empty cart refused
| Given | When | Then |
|---|---|---|
| An order with no items | Priced (preview) | Refused - an empty cart cannot be priced |
AC-SIM-06: v2 best-effort at checkout
| Given | When | Then |
|---|---|---|
| Checkout pricing where v2 calculation fails | Order is priced | The v1 result still returns and checkout proceeds; the v2 failure is logged and skipped |
AC-SIM-07: Upstream rejection surfaced
| Given | When | Then |
|---|---|---|
| A line whose variant has no active fare set | Priced | The real upstream status and reason are surfaced (not a generic 500) |
PROMO - Promotions & Rules In-progress
Feature ID: pricing/PROMO · Phase: P3 · PRDs: Promotions, methods & segment rules · Dev: @nx/pricing · Promotion System
What it does for users: owners define promotion campaigns - a promotion carries exactly one method (fixed or percentage; targeting items, the order, or shipping; with an allocation strategy), a standard or buy-get scheme, and three rule sets that segment who qualifies (eligibility), what must be bought (source), and what gets discounted (target). CRUD and the aggregate (promotion + method + all rule sets) are available today; the discount calculator that applies promotions at checkout is not yet wired into the pricing pipeline.
Requirements
| ID | P | Requirement |
|---|---|---|
| URD-PROMO-001 | M | Owner can create a promotion with a method and rules in one aggregate operation. |
| URD-PROMO-002 | M | Owner can update a promotion aggregate (method + rules: add/update/remove). |
| URD-PROMO-003 | M | A promotion has at most one promotion method. |
| URD-PROMO-004 | S | Eligibility rules use the same rule model as fares (attribute, operator, value, AND logic). |
| URD-PROMO-005 | M | Promotions, methods, and rules are isolated per merchant and use soft-delete. |
| URD-PROMO-006 | W | Promotion discounts are applied automatically during checkout pricing (calculator pending - not yet wired). |
| URD-PROMO-007 | M | A promotion declares a scheme - STANDARD (a straight discount) or BUY_GET (buy-X-get-Y) - defaulting to STANDARD. |
| URD-PROMO-008 | M | The single method declares how the discount is computed: FIXED (an amount off) or PERCENTAGE (a percent off), carrying the discount value. |
| URD-PROMO-009 | M | The method declares what the discount lands on via a target type: ITEMS, ORDER, or SHIPPING. |
| URD-PROMO-010 | S | The method declares an allocation strategy - EACH (per item), ACROSS (spread over items), or ONCE (a single item). |
| URD-PROMO-011 | S | The method can cap the number of discounted units via a maximum-quantity limit. |
| URD-PROMO-012 | M | A BUY_GET method declares a source minimum quantity (what must be bought) and a target quantity (what becomes discounted). |
| URD-PROMO-013 | M | Method-level rules are split into source rules (what must be bought) and target rules (what gets discounted), tagged by context, alongside the promotion's eligibility rules. |
| URD-PROMO-014 | M | Eligibility, source, and target rule counts are denormalized on the promotion and method and kept accurate as rules are added or removed. |
| URD-PROMO-015 | S | A promotion carries an optional code; a promotion with no code is automatic (auto-apply) only, and a code is unique per merchant among live promotions. |
| URD-PROMO-016 | S | A promotion carries a stacking flag controlling whether it may combine with other promotions, and a tax-inclusive flag. |
| URD-PROMO-017 | S | A promotion carries an effective-from / effective-to window, a usage limit, and a running usage count. |
| URD-PROMO-018 | M | A promotion moves through a lifecycle status - DRAFT, ACTIVATED, DEACTIVATED, EXPIRED, or ARCHIVED - defaulting to DRAFT. |
| URD-PROMO-019 | M | In an aggregate update each rule entry is interpreted by shape: no id creates, id-plus-fields updates, id-only deletes; a created rule must carry attribute, operator, and data type. |
| URD-PROMO-020 | M | Deleting a promotion cascades to its method and every attached rule (eligibility, source, target) in one transaction. |
Acceptance
AC-PROMO-01: Promotion aggregate
| Given | When | Then |
|---|---|---|
| Owner submits a promotion + method + 2 rules | Created | Promotion, its method, and both rules persist together |
| Owner removes one rule via aggregate update | Saved | The rule is soft-deleted; the rest are preserved |
AC-PROMO-02: Buy-get with source/target rules
| Given | When | Then |
|---|---|---|
| A BUY_GET promotion: one PERCENTAGE method (value 100, source-min 2, target 1) + 1 source rule + 1 target rule | Created via aggregate | Promotion, method, source rule, and target rule persist together; source and target rule counts each read 1 |
AC-PROMO-03: Aggregate rule merge by shape
| Given | When | Then |
|---|---|---|
| An existing promotion with two eligibility rules | Aggregate update sends one rule with id+fields, one with id only, one with no id | The first is updated, the second deleted, the third created - in one transaction; the rule count stays accurate |
| A created rule missing attribute / operator / data type | Aggregate update saved | The whole aggregate is refused and rolled back |
AC-PROMO-04: Code uniqueness & automatic
| Given | When | Then |
|---|---|---|
| A live promotion with code SUMMER | Creating another live promotion with code SUMMER | Refused - code is unique per merchant among non-deleted promotions |
| A promotion with no code | Saved | It is automatic (auto-apply) only |
7. Constraints & Non-Goals
Cross-cutting requirements (
CON) apply to all features above and are tested asTC-CON-*.
Cross-cutting requirements (CON)
| ID | P | Requirement |
|---|---|---|
| URD-CON-001 | M | All pricing endpoints require authentication (JWT or basic auth). |
| URD-CON-002 | M | All records are scoped per merchant via the merchant header and use soft-delete. |
| URD-CON-003 | M | Monetary values use decimal precision (4 places) throughout calculation. |
| URD-CON-004 | M | Pricing snapshots (v2) are immutable once computed. |
Constraints
| ID | Constraint |
|---|---|
| C-01 | Exactly one activated fare set per product variant. |
| C-02 | At most one current (open-ended) cost per variant at a time. |
| C-03 | Taxes apply in ascending priority order; inclusive taxes never increase the total. |
| C-04 | Money math uses float(value, 4) precision. |
| C-05 | All records are merchant-isolated and soft-deleted. |
| C-06 | v1 (/simulation) and v2 (/simulation-v2) share core fare/tax logic; fixes land in both. |
Non-Goals
- Multi-currency pricing
- A discount calculator wired into checkout (entities exist; not yet applied)
- Order persistence, invoice issuance, stock or payment side effects
8. Version History
| Date | Author | Description | Ver |
|---|---|---|---|
| 2026-06-05 | Claude (AI pair) | Initial URD from @nx/pricing code analysis; feature spine FARE / TAX / COST / PROMO | v0.1 |
| 2026-06-15 | Pricing squad | Added SIM feature - pricing simulation (v1 flat + v2 snapshot) & order pricing preview | v0.2 |