PRD: Fare system & pricing
| Module | Product (CORE-05) | PRD ID | PRD-FAR-001 |
| Status | Shipped | Owner | Product squad |
| Date | 2026-06-03 | Version | v1.0 |
| Packages | @nx/pricing · @nx/commerce · @nx/sale · apps/client | URD | FAR · VAR |
TL;DR
Gives every sellable variant a first-class fare set - a pricing container with a default price plus date-ranged, quantity-tiered, and context-specific overrides and discounts - and a snapshot pricing engine that resolves the price a cart line should pay at sale time. A CDC seam keeps each variant's fare set provisioned automatically. The result: deterministic, tiered POS pricing instead of a single scalar price per variant.
1. Context & Problem
Variants need prices, but a single scalar price is not enough for POS. The same variant may carry a base price plus date-ranged, quantity-tiered, and context-specific overrides and discounts (channel, time, service duration), and the applicable price must resolve deterministically at sale time. Today the catalogue (commerce) has variants but no first-class pricing container, and there is no engine to compute the price a cart line should actually pay - pricing logic would otherwise leak into every selling channel and drift.
This increment builds the pricing side of the catalogue: a fare set linked one-to-one to each variant, fares inside it (a default plus override/discount groups with context rules), a snapshot pricing engine that resolves the applicable fare at sale time, and a CDC seam so that creating or updating a product variant in commerce automatically provisions and enriches its fare set in pricing.
2. Goals & Non-Goals
Goals
- Fare-set CRUD: every variant carries exactly one fare set with at least one default fare (URD-FAR-001…002).
- Tiered fares: effective date ranges, min/max quantity windows, and parent-child fare groups with context rules (channel, time, quantity, service duration) (URD-FAR-005…007).
- Snapshot pricing engine: fare calculator + tax calculator + a pricing v2 endpoint that resolves the applicable fare at sale time (override → discount → default) (URD-FAR-003…004).
- CDC seam: product-variant create/update in commerce cascades into pricing to create and enrich the variant's fare set (including bundle / frequently-bought-together links).
- Client fare UI: fare-set panel, default-price wizard, tier/group editing, and rule selection (channel / time / service-duration) surfaced in product edit.
Non-Goals
- Promotion discount calculation at pricing time - promotion CRUD only; the discount engine stays disabled (owned by Promotions).
- Unit-conversion engine and a standalone "resolved price" read endpoint for a variant (URD-VAR-011/012 - Planned).
- CSV / bulk catalogue import.
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Fare-set coverage | 100% of variants carry exactly one fare set with at least one default fare |
| Resolution determinism | The same cart line + context always resolves to the same fare (override → discount → default) |
| Seam reliability | Variant create/update always provisions/enriches the fare set; zero variants without a fare set |
| Sale-time pricing | Sale-order preview returns a priced line for every variant, including FBT bundles |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Set base prices and tiered overrides/discounts per variant; control how prices vary by channel, time, and quantity |
| Cashier / Employee | Get the correct price applied automatically at the point of sale |
| Sale channel (system) | Resolve the price a cart line should pay given the sale context |
Core scenarios: an owner defines a variant's default price and tiers in product edit → the fare set is provisioned/enriched via the CDC seam → at sale time the pricing engine resolves the applicable fare for the cart line and context → the sale order previews and charges the resolved price.
5. User Stories
- As an owner, I want every variant to carry a fare set with a default price, so that nothing is sold without a price.
- As an owner, I want to add date-ranged and quantity-tiered fares, so that prices change for promo windows and bulk purchases.
- As an owner, I want fare groups with context rules (channel, time, service duration), so that the same variant prices differently per channel or time.
- As a cashier, I want the correct price resolved automatically when a variant is added to the cart, so that I never key prices manually.
- As a sale channel, I want a pricing preview endpoint that accepts the sale context, so that an order line shows the price it will actually pay (including FBT bundles).
- As an owner, I want creating or updating a variant to provision its fare set automatically, so that pricing never lags behind the catalogue.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Every variant has exactly one fare set; a fare set contains at least one default fare | URD-FAR-001..002 |
| FR-2 | Fare amount must be zero or positive | URD-FAR-003 |
| FR-3 | At sale time the engine resolves the applicable fare: override → discount → default | URD-FAR-004 |
| FR-4 | Fares support effective date ranges and min/max quantity windows | URD-FAR-005..006 |
| FR-5 | Fares support parent-child groups (OVERRIDE / DISCOUNT) with context rules (channel, time, quantity, service duration) | URD-FAR-007 |
| FR-6 | Snapshot pricing engine: fare calculator + tax calculator + pricing v2 endpoint compute the line price from a pricing snapshot | URD-FAR-004 |
| FR-7 | Sale-order pricing preview accepts a pricing context (serviceTime, channel) and prices FBT via orderProductVariantIds | URD-FAR-004 |
| FR-8 | CDC seam: variant create/update in commerce provisions and enriches the fare set (incl. bundle / FBT links) | URD-VAR-003/004 |
Full requirement text and acceptance criteria live in the Product URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Data integrity | Every variant invariably carries exactly one fare set with at least one fare; fare amounts are zero or positive |
| Determinism | Fare resolution is deterministic for a given line + context (override → discount → default; date/quantity windows filter candidates first) |
| Tenancy & authz | All operations scoped per merchant (x-merchant-id); gated by product/pricing permissions |
| Precision | Monetary/quantity math uses float(value, 4) |
| Consistency | Variant + fare-set provisioning is part of the atomic variant aggregate; the CDC seam reconciles fare sets to variant state |
| i18n | User-facing fare labels are bilingual ({ en, vi }) |
8. UX & Flows
Key screens (in apps/client): fare-set panel, default-price wizard, tier/group editing, and rule selection (channel / time / service-duration), surfaced in product edit.
9. Data & Domain
| Entity | Role |
|---|---|
FareSet | Pricing container linked one-to-one to a variant; holds the variant's fares |
Fare | A price entry - default, override, or discount; carries amount, date range, quantity window |
| Fare group | Parent-child grouping (OVERRIDE / DISCOUNT) with context rules (channel, time, quantity, service duration) |
| Pricing snapshot | Immutable input to the calculator that resolves the applicable fare at sale time |
Conceptual only - full schema and invariants in the pricing fares domain model.
10. Dependencies & Assumptions
Depends on
- Variants (URD-VAR) - a fare set is provisioned per variant via the aggregate create/update.
- Commerce → Pricing CDC seam (
@nx/commerce→@nx/pricing) - Kafka CDC of product variants drives fare-set provisioning. - Sale orders (
@nx/sale) - consumes the pricing preview endpoint with sale context.
Assumptions
- Each variant exists in commerce before its fare set is enriched (the CDC seam orders this).
- The sale channel supplies a pricing context (serviceTime, channel) and
orderProductVariantIdsfor FBT pricing.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Fare set could lag behind a newly created/updated variant | CDC seam provisions/enriches the fare set on every variant change |
| Ambiguous resolution when multiple fares qualify | Fixed precedence override → discount → default; date/quantity windows filter candidates first |
| Promotion discounts not applied at pricing time | Out of scope this increment; discount engine disabled, promotion CRUD only |
| No standalone resolved-price read endpoint for a variant | Planned (URD-VAR-011); pricing is exposed via the sale-order preview today |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 - see URD feature catalog |
| Rollout | All merchants; no feature flag |
| Migration | None (fare sets provisioned per variant via the CDC seam) |
| Launch criteria | Variant create/update provisions a fare set; engine resolves override → discount → default; sale-order preview prices lines (incl. FBT); fare amounts zero or positive |
| Monitoring | Variants-without-fare-set checks, fare-resolution outcomes, pricing-preview error rate |
13. FAQ
How is the applicable price chosen? The engine filters fares by date range and quantity window first, then resolves in precedence order: an override group wins; otherwise the lowest qualifying discount; otherwise the default (base) fare.
Can a variant exist without a fare set? No - every variant carries exactly one fare set with at least one default fare, provisioned through the CDC seam.
Are promotions applied at pricing time? Not yet - promotion CRUD exists, but the discount calculation engine is disabled, so promotion discounts are not applied automatically.
Where does pricing run at sale time? The sale order calls the pricing preview endpoint with a pricing context (serviceTime, channel); FBT bundles are priced via orderProductVariantIds.
OVERRIDE vs DISCOUNT fare groups - what's the difference? OVERRIDE groups replace the price outright; DISCOUNT groups reduce it. Both can carry channel / time / quantity / service-duration rules.
References
- URD: Product - Fares / Pricing · Variants
- Builds on: Variants
- Related PRD: Promotions
- Module: Product - overview + traceability
- Developer: pricing fares · @nx/pricing · @nx/commerce