PRD: Fare & Tax Pricing Engine
| Module | Pricing (CORE-14) | PRD ID | PRD-FARE-001 |
| Status | Shipped | Owner | Pricing squad |
| Date | 2026-06-05 | Version | v1.0 |
| Packages | @nx/pricing | URD | FARE · TAX |
TL;DR
Gives a merchant one engine that turns any product variant into money: it picks the winning fare per line (base, override, or rule-driven discount), layers on the right taxes (inclusive/exclusive, compound, item vs order level), and returns a single immutable pricing snapshot. The sale flow asks for that result at checkout instead of re-implementing pricing, so every line and order is priced consistently and auditably.
1. Context & Problem
Prices were previously hard-coded on the product or computed ad hoc at checkout, with no consistent place for taxes, rule-based prices, or an auditable breakdown. Merchants need a single engine that selects the right fare for each variant, computes taxes correctly (inclusive/exclusive, compound, item vs order level), and returns one consistent priced result. The sale flow must ask for that result at checkout without re-implementing pricing logic - a hard requirement for reliable cost, tax, and margin reporting in the HKD/SME bookkeeping KICKO targets.
This engine builds fare selection and tax computation on top of the product-variant catalog and the per-merchant commerce scope, and exposes a simulation endpoint the sale flow consumes.
2. Goals & Non-Goals
Goals
- Select the winning fare per variant: default, OVERRIDE (first valid child), or DISCOUNT (cheapest valid child), with rule evaluation (attribute, operator, value; AND logic).
- Compute taxes in priority order with percentage/fixed/combined modes, inclusive/exclusive treatment, compound tax-on-tax, and item vs order scope.
- Expose a simulation endpoint that returns an immutable pricing snapshot - per-line applied fares and taxes plus order totals.
- Auto-seed a fare set + default fare when a product variant is created (CDC), so every variant is immediately priceable.
- Apply a default tax rate fallback when no tax set is configured.
Non-Goals
- Applying promotion discounts during checkout - the calculator is not wired this increment (owned by the Promotions feature).
- Multi-currency pricing.
- Persisting orders, issuing invoices, or mutating stock.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Pricing correctness | Checkout prices match configured fares + taxes for sampled baskets; zero discrepancies vs manual calculation |
| Fare coverage | 100% of product variants are immediately priceable (auto-seeded fare set + default fare) |
| Snapshot integrity | Every priced order produces an immutable v2 snapshot with a per-line fare/tax breakdown |
| Precision | Monetary math agrees to 4 decimal places end-to-end |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner / Manager | Set base and conditional prices per variant, attach taxes, control how prices are selected |
| Cashier | Get a correct, consistent priced result at checkout without configuring anything |
| System | Auto-seed a fare set + default fare when a variant appears, so nothing is unpriceable |
Core scenarios: an owner sets a base fare and optional rule-driven child fares + taxes per variant → at checkout the sale flow calls the simulation endpoint → the engine selects the winning fare per line, computes item and order taxes in priority order → returns an immutable pricing snapshot with per-line and order totals.
5. User Stories
- As an owner, I want to set a base price per variant and optional conditional child prices driven by rules, so the right price applies automatically.
- As an owner, I want to choose whether the first matching child fare wins (OVERRIDE) or the cheapest valid child wins (DISCOUNT), so I control my pricing strategy.
- As an owner, I want to attach percentage/fixed/compound taxes at item or order level, inclusive or exclusive, so totals reflect the correct tax treatment.
- As a cashier, I want checkout to return a consistent priced result for the basket, so I never re-key or re-calculate prices.
- As a cashier, I want an auditable breakdown of every applied fare and tax, so a disputed price can be explained.
- As the system, I want a fare set + default fare created automatically when a variant appears, so every variant is immediately priceable.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Each product variant has exactly one activated fare set; an owner can set a default (base) fare | URD-FARE-001 · URD-FARE-003 |
| FR-2 | Auto-seed a fare set + default fare when a product variant is created (CDC) | URD-FARE-002 |
| FR-3 | Parent fares carry an OVERRIDE or DISCOUNT strategy; child fares carry conditional prices and rules (AND logic) | URD-FARE-004..006 |
| FR-4 | OVERRIDE selects the first valid child; DISCOUNT selects the cheapest valid child; default fare is the fallback | URD-FARE-007..009 |
| FR-5 | Owner can create tax types and a tax set, adding taxes via aggregate update | URD-TAX-001..002 |
| FR-6 | A tax can be percentage, fixed, or combined; taxes apply in ascending priority order | URD-TAX-003..004 |
| FR-7 | A tax can be exclusive (added on top) or inclusive (back-calculated); compound taxes compute on the running total | URD-TAX-005..006 |
| FR-8 | Item-level taxes apply per line; order-level taxes apply to merchant-scoped sets | URD-TAX-008 |
| FR-9 | A default tax rate is applied when no tax set is configured | URD-TAX-009 |
| FR-10 | The simulation endpoint returns an immutable v2 pricing snapshot (per-line applied fares + taxes, plus order totals); v1 returns a flat result | URD-CON-004 |
| FR-11 | Monetary values use 4-decimal precision throughout calculation | URD-CON-003 |
Full requirement text and acceptance criteria live in the Pricing URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | A computed v2 pricing snapshot is immutable once produced; the breakdown is not edited after the fact |
| Tenancy & authz | All fares, taxes, and records are scoped per merchant (merchant header) and use soft-delete; endpoints require authentication |
| Precision | Monetary math uses float(value, 4); inclusive taxes never increase the total |
| Performance / scale | Simulation prices a full basket in one call; v1 and v2 share core fare/tax logic so fixes land in both |
| Consistency | Exactly one activated fare set per variant; auto-seeding keeps every variant priceable |
| i18n | User-facing labels are bilingual ({ en, vi }) |
8. UX & Flows
Configuration screens (fares, fare sets, taxes, tax sets) live in the owner-facing back office; the priced result is consumed headlessly by the sale flow at checkout. Both /simulation (v1, flat) and /simulation-v2 (snapshot) are live, with v2 canonical.
9. Data & Domain
| Entity | Role |
|---|---|
FareSet | Container of all fares for one product variant; exactly one activated per variant |
Fare | A price entry - default (base), or parent/child forming a selection group |
Rule | A condition (attribute, operator, value) on a child fare; all rules must pass (AND) |
TaxSet | Container of taxes for a variant (item-level) or a merchant (order-level) |
Tax | A percentage/fixed/combined charge with priority, temporal window, inclusive/exclusive/compound flags |
TaxType | A category (e.g. VAT) - system-wide or merchant-scoped |
| Pricing snapshot | The immutable v2 result - applied fares and taxes per line, plus order totals |
Conceptual only - full schema and invariants in the pricing domain model.
10. Dependencies & Assumptions
Depends on
- Product (variants) - fares, tax sets, and costs are keyed to product variants; variant CDC drives auto-seeding.
- Commerce / Merchant - fares, taxes, and records are scoped per merchant.
- Product-variant CDC stream - the trigger that auto-seeds a fare set + default fare.
Assumptions
- Every product variant emits a CDC event on creation so its fare set can be seeded.
- A merchant default tax rate exists to apply when no tax set is configured.
- The sale flow calls the simulation endpoint at checkout rather than computing prices itself.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Promotion discount ordering vs order-level taxes in the v2 pipeline | Open: define whether the discount calculator runs before or after order-level taxes when it lands |
| Two live simulation versions (v1 + v2) could drift | v1 and v2 share core fare/tax logic; fixes land in both. Open: deprecate v1 once all consumers move to v2 |
| Missing tax configuration would leave a line untaxed | A default tax rate is applied when no tax set is configured (FR-9) |
| Variant created without a fare would be unpriceable | Auto-seed a fare set + default fare on variant CDC (FR-2) |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P1 (Fares + Taxes) - see URD feature catalog |
| Rollout | All merchants; no feature flag. Both /simulation (v1) and /simulation-v2 live, v2 canonical |
| Migration | None (new entities; fare sets auto-seeded from product-variant CDC) |
| Launch criteria | Checkout prices match configured fares + taxes for sampled baskets; v2 snapshot produced per order; tax inclusive/exclusive/compound math verified |
| Monitoring | Pricing discrepancy rate vs manual calculation, fare-seeding coverage per merchant, snapshot generation errors |
13. FAQ
How does the engine pick which fare wins? It evaluates child fares against their rules. For an OVERRIDE parent the first valid child wins; for a DISCOUNT parent the cheapest valid child wins; if no child is valid, the default fare is used.
What happens if a variant has no taxes configured? A default tax rate is applied so the line is never left untaxed.
Inclusive vs exclusive tax - what's the difference? Exclusive taxes are added on top of the price; inclusive taxes are back-calculated out of the price, so an inclusive tax never increases the total.
What is the difference between v1 and v2 simulation? v1 (/simulation) returns a flat priced result; v2 (/simulation-v2) returns an immutable snapshot with a per-line applied-fare and tax breakdown plus order totals. v2 is canonical.
Does this engine apply promotions at checkout? Not yet - promotion entities and CRUD exist, but the discount calculator is not wired into the pricing pipeline this increment.
References
- URD: Pricing - Fares & Fare Sets · Tax Computation
- Related: Cost Tracking · Promotions & Rules
- Module: Pricing - overview + traceability
- Developer: @nx/pricing · domain model · Fare System · Tax System