Fare System
1. Overview
| Property | Value |
|---|---|
| Status | Stable (canonical pricing input) |
| Owner | pricing-team |
| Services | FareService (CRUD), PricingFareCalculatorService (v1), FareCalculatorService (v2), PricingRuleEvaluatorService |
| Controllers / Routes | FareController /fares, FareSetController /fare-sets, RuleController /rules |
| Depends on | FareSet, Fare, Rule schemas (@nx/core) |
Manages pricing through FareSets (one container per product variant), Fares (price entries in a parent/child hierarchy), and Rules (context conditions on child fares). Supports static prices, quantity discounts, time-based pricing, and channel/FBT overrides.
Service catalogue and identity card: see Pricing Service index. REST reference: live at
/v1/api/pricing/doc/openapi.json.
2. Data Model
2.1. Fare Types
| Type | parentId | childrenCount | type | amount | Role |
|---|---|---|---|---|---|
| Default | null | null | null | set | Base price — fallback when no rules match |
| Parent | null | > 0 | OVERRIDE / DISCOUNT | null | Group container — defines selection strategy |
| Child | set | null | null | set | Variant price — selected when rules pass |
2.2. Parent Types
| Type | Behavior |
|---|---|
OVERRIDE | Highest priority — first valid child is selected immediately |
DISCOUNT | Children compete — lowest price among valid children is selected |
3. Fare Selection Flow
3.1. Selection Result
type TFareSelectionResult = {
fareSet: TFareSet;
selectedFare: TFare; // The fare chosen
baseFare: TFare; // The default fare (same as fallback fare)
appliedRules: TRule[]; // Rules that matched
selectionReason: 'default' | 'override' | 'discount';
};| Field | Type | Description |
|---|---|---|
fareSet | TFareSet | The activated fare set for the product variant |
selectedFare | TFare | The fare ultimately chosen by the algorithm |
baseFare | TFare | The default fare (useful for discount comparison) |
appliedRules | TRule[] | Rules that evaluated true for the selected child |
selectionReason | 'default' | 'override' | 'discount' | Why this fare was selected |
3.2. Pre-filtering by Date and Quantity
Before fare selection begins, the fare repository filters active fares using two criteria:
| Filter | Condition | Notes |
|---|---|---|
effectiveDate | effectiveFrom <= date <= effectiveTo | effectiveTo = null means no expiry |
quantity | minQuantity <= qty <= maxQuantity | null limits are treated as unbounded |
Only fares passing both filters are considered during rule evaluation. This reduces the candidate pool before any rule logic runs.
Tip: Use
minQuantity/maxQuantityon child fares to ensure they are never even loaded when a request falls outside the intended quantity range — this is more efficient than relying solely on rule operators.
4. Fare Groups
A fare group is a parent fare with one or more child fares. Each child can have rules that determine when it applies.
4.1. Create Fare Group
POST /fares/groups
Content-Type: application/json
Authorization: Bearer <token>
{
"fareSetId": "fareset-123",
"parent": {
"name": "Bulk Discount",
"type": "DISCOUNT",
"status": "ACTIVATED"
},
"children": [
{
"name": "10+ units",
"amount": "90000",
"minQuantity": "10",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "10",
"priority": 1
}
]
},
{
"name": "50+ units",
"amount": "80000",
"minQuantity": "50",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "50",
"priority": 1
}
]
}
]
}4.2. Creation Steps
| Step | Action | Details |
|---|---|---|
| 1 | Create parent fare | fareSetId, type, status — no amount |
| 2 | Create child fares | For each child in array |
| 3 | Create rules | Batch create rules with principalId=child.id, principalType='Fare' |
| 4 | Update rulesCount | Set fare.rulesCount for each child |
| 5 | Update childrenCount | Count and set parent.childrenCount |
4.3. Add Child to Existing Group
POST /fares/children
Content-Type: application/json
Authorization: Bearer <token>
{
"parentId": "fare-parent-456",
"name": "100+ units",
"amount": "70000",
"minQuantity": "100",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "100",
"priority": 1
}
]
}5. Rule Evaluation
Rules are evaluated with AND logic — all rules on a child fare must pass for that child to be valid.
5.1. Operators
| Operator | Description | Example |
|---|---|---|
EQ | Equal | merchantId EQ "m-123" |
NE / NEQ | Not equal | status NEQ "blocked" |
GT | Greater than | quantity GT 5 |
GTE | Greater than or equal | quantity GTE 10 |
LT | Less than | quantity LT 100 |
LTE | Less than or equal | quantity LTE 50 |
IN / INQ | In set | saleChannelId IN ["ch-1", "ch-2"] |
NIN | Not in set | merchantId NIN ["blocked-1"] |
5.2. Data Types
| Type | Field | Use Case |
|---|---|---|
TEXT | tValue | Match channel IDs, merchant IDs, string attributes |
NUMBER | nValue | Quantity thresholds, price ranges |
BOOLEAN | bValue | Feature flags, membership status |
JSON | jValue | Complex nested conditions |
5.3. Context Matching
The rule evaluator extracts values from the pricing context using lodash get():
// Rule: { attribute: "quantity", operator: "GTE", nValue: "10" }
// Context: { quantity: "15", merchantId: "m-123", saleChannelId: "ch-1" }
// → get(context, "quantity") = "15" → 15 >= 10 → PASS6. Product Variant Integration
When a product variant is created, the fare system automatically sets up pricing:
7. Controller API
Pricing calculations run via POST /simulation/calculate (v1) and POST /simulation-v2/calculate (v2). Fare management runs through:
FareController/fares— CRUD + customPOST /fares/groups(parent + children) andPOST /fares/children(add child to existing parent).FareSetController/fare-sets— CRUD.RuleController/rules— CRUD.
Full request/response reference is rendered live from /v1/api/pricing/doc/openapi.json — not hand-maintained here.
8. Service Dependencies
9. Practical Examples
9.1. Scenario 1: Quantity-Based Bulk Discount
Business Rule: "Buy 10+ units get 10% off, 50+ units get 20% off, 100+ units get 30% off"
Step 1: Create Fare Group
POST /fares/groups
Content-Type: application/json
Authorization: Bearer <token>
{
"fareSetId": "fareset-laptop-001",
"parent": {
"name": "Bulk Discount Tiers",
"type": "DISCOUNT",
"status": "ACTIVATED"
},
"children": [
{
"name": "10-49 units (10% off)",
"amount": "90000",
"minQuantity": "10",
"maxQuantity": "49",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "10",
"priority": 1
},
{
"attribute": "quantity",
"operator": "LTE",
"dataType": "NUMBER",
"nValue": "49",
"priority": 2
}
]
},
{
"name": "50-99 units (20% off)",
"amount": "80000",
"minQuantity": "50",
"maxQuantity": "99",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "50",
"priority": 1
},
{
"attribute": "quantity",
"operator": "LTE",
"dataType": "NUMBER",
"nValue": "99",
"priority": 2
}
]
},
{
"name": "100+ units (30% off)",
"amount": "70000",
"minQuantity": "100",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "100",
"priority": 1
}
]
}
]
}Step 2: Calculate Pricing via Simulation
Note: Pricing calculations are now performed via
POST /simulation/calculate. See Simulation Endpoint for details.
Example selection result:
{
"fareSet": { "id": "fareset-laptop-001", "status": "ACTIVATED" },
"selectedFare": {
"id": "fare-child-002",
"name": "50-99 units (20% off)",
"amount": "80000",
"minQuantity": "50",
"maxQuantity": "99"
},
"baseFare": {
"id": "fare-default-001",
"amount": "100000"
},
"appliedRules": [
{ "attribute": "quantity", "operator": "GTE", "nValue": "10" },
{ "attribute": "quantity", "operator": "LTE", "nValue": "99" }
],
"selectionReason": "discount"
}9.2. Scenario 2: Time-Based Dynamic Pricing
Business Rule: "Early bird (6-9 AM) = 20% off, Peak hours (12-2 PM) = 30% premium, Late night (10 PM - 1 AM) = 15% off"
Step 1: Create Time-Based Fare Group
POST /fares/groups
Content-Type: application/json
{
"fareSetId": "fareset-ticket-001",
"parent": {
"name": "Time-Based Pricing",
"type": "OVERRIDE",
"status": "ACTIVATED"
},
"children": [
{
"name": "Early Bird Special",
"amount": "80000",
"effectiveFrom": "2026-01-01T06:00:00Z",
"effectiveTo": "2026-12-31T09:00:00Z",
"status": "ACTIVATED",
"rules": [
{
"attribute": "requestTime",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "06:00",
"priority": 1
},
{
"attribute": "requestTime",
"operator": "LT",
"dataType": "TEXT",
"tValue": "09:00",
"priority": 2
}
]
},
{
"name": "Peak Hours Premium",
"amount": "130000",
"status": "ACTIVATED",
"rules": [
{
"attribute": "requestTime",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "12:00",
"priority": 1
},
{
"attribute": "requestTime",
"operator": "LT",
"dataType": "TEXT",
"tValue": "14:00",
"priority": 2
}
]
},
{
"name": "Late Night Discount",
"amount": "85000",
"status": "ACTIVATED",
"rules": [
{
"attribute": "requestTime",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "22:00",
"priority": 1
}
]
}
]
}Step 2: Calculate Pricing via Simulation
Note: Pricing calculations are now performed via
POST /simulation/calculate. See Simulation Endpoint for details.
Example selection result:
{
"selectedFare": {
"id": "fare-peak-001",
"name": "Peak Hours Premium",
"amount": "130000"
},
"baseFare": { "amount": "100000" },
"selectionReason": "override"
}9.3. Scenario 3: Channel-Specific Pricing
Business Rule: "Online = base price, In-store kiosk = +10%, Phone order = +15%, Partner channel = -5%"
Step 1: Create Channel-Based Fare Group
POST /fares/groups
Content-Type: application/json
{
"fareSetId": "fareset-product-001",
"parent": {
"name": "Channel Pricing",
"type": "OVERRIDE",
"status": "ACTIVATED"
},
"children": [
{
"name": "Kiosk Premium",
"amount": "110000",
"status": "ACTIVATED",
"rules": [
{
"attribute": "saleChannelId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "ch-kiosk-001",
"priority": 1
}
]
},
{
"name": "Phone Order Premium",
"amount": "115000",
"status": "ACTIVATED",
"rules": [
{
"attribute": "saleChannelId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "ch-phone-001",
"priority": 1
}
]
},
{
"name": "Partner Discount",
"amount": "95000",
"status": "ACTIVATED",
"rules": [
{
"attribute": "saleChannelId",
"operator": "IN",
"dataType": "JSON",
"jValue": ["ch-partner-001", "ch-partner-002"],
"priority": 1
}
]
}
]
}Step 2: Calculate Pricing via Simulation
Note: Pricing calculations are now performed via
POST /simulation/calculate. See Simulation Endpoint for details.
Example selection result:
{
"selectedFare": {
"id": "fare-kiosk-001",
"name": "Kiosk Premium",
"amount": "110000"
},
"baseFare": { "amount": "100000" },
"appliedRules": [
{ "attribute": "saleChannelId", "operator": "EQ", "tValue": "ch-kiosk-001" }
],
"selectionReason": "override"
}9.4. Scenario 4: Multi-Rule Combination (Quantity + Time + Channel)
Business Rule: "VIP channel customers buying 20+ units during weekday mornings get special pricing"
Step 1: Create Complex Fare Group
POST /fares/groups
Content-Type: application/json
{
"fareSetId": "fareset-premium-001",
"parent": {
"name": "VIP Bulk Morning Deal",
"type": "DISCOUNT",
"status": "ACTIVATED"
},
"children": [
{
"name": "VIP Bulk Morning Price",
"amount": "75000",
"minQuantity": "20",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "20",
"priority": 1
},
{
"attribute": "saleChannelId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "ch-vip-001",
"priority": 2
},
{
"attribute": "requestTime",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "06:00",
"priority": 3
},
{
"attribute": "requestTime",
"operator": "LT",
"dataType": "TEXT",
"tValue": "12:00",
"priority": 4
},
{
"attribute": "dayOfWeek",
"operator": "IN",
"dataType": "JSON",
"jValue": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"priority": 5
}
]
}
]
}Step 2: Calculate Pricing via Simulation (All Conditions Met)
Note: Pricing calculations are now performed via
POST /simulation/calculate. See Simulation Endpoint for details.
Example selection result (all rules pass — AND logic):
{
"selectedFare": {
"id": "fare-vip-bulk-001",
"name": "VIP Bulk Morning Price",
"amount": "75000"
},
"baseFare": { "amount": "100000" },
"appliedRules": [
{ "attribute": "quantity", "operator": "GTE", "nValue": "20" },
{ "attribute": "saleChannelId", "operator": "EQ", "tValue": "ch-vip-001" },
{ "attribute": "requestTime", "operator": "GTE", "tValue": "06:00" },
{ "attribute": "requestTime", "operator": "LT", "tValue": "12:00" },
{ "attribute": "dayOfWeek", "operator": "IN", "jValue": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] }
],
"selectionReason": "discount"
}Step 3: Calculate Pricing via Simulation (One Rule Fails — Saturday)
When
dayOfWeekis"Saturday", the weekday rule fails. The simulation falls back to the default fare.
Example selection result (falls back to default — dayOfWeek rule failed):
{
"selectedFare": {
"id": "fare-default-001",
"amount": "100000"
},
"baseFare": { "amount": "100000" },
"appliedRules": [],
"selectionReason": "default"
}9.5. Scenario 5: Seasonal Campaign with Date Range
Business Rule: "Summer sale (June-August): 25% off all purchases"
Step 1: Create Seasonal Fare
POST /fares/groups
Content-Type: application/json
{
"fareSetId": "fareset-seasonal-001",
"parent": {
"name": "Seasonal Campaigns",
"type": "OVERRIDE",
"status": "ACTIVATED"
},
"children": [
{
"name": "Summer Sale 2026",
"amount": "75000",
"effectiveFrom": "2026-06-01T00:00:00Z",
"effectiveTo": "2026-08-31T23:59:59Z",
"status": "ACTIVATED",
"rules": [
{
"attribute": "effectiveDate",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "2026-06-01",
"priority": 1
},
{
"attribute": "effectiveDate",
"operator": "LTE",
"dataType": "TEXT",
"tValue": "2026-08-31",
"priority": 2
}
]
}
]
}Step 2: Calculate Pricing via Simulation (During Campaign)
Note: Pricing calculations are now performed via
POST /simulation/calculate. See Simulation Endpoint for details.
Example selection result:
{
"selectedFare": {
"id": "fare-summer-001",
"name": "Summer Sale 2026",
"amount": "75000",
"effectiveFrom": "2026-06-01T00:00:00Z",
"effectiveTo": "2026-08-31T23:59:59Z"
},
"baseFare": { "amount": "100000" },
"selectionReason": "override"
}Step 3: Calculate Pricing via Simulation (After Campaign)
When
effectiveDateis"2026-09-01", the fare'seffectiveTofilter excludes it from the candidate pool. The simulation falls back to the default fare.
Example selection result (campaign expired — falls back to default):
{
"selectedFare": {
"id": "fare-default-001",
"amount": "100000"
},
"baseFare": { "amount": "100000" },
"appliedRules": [],
"selectionReason": "default"
}9.6. Scenario 6: Adding Child Fare to Existing Group
Business Rule: "Add a new tier to existing bulk discount: 200+ units = 40% off"
Step 1: Add Child to Existing Parent
POST /fares/children
Content-Type: application/json
{
"parentId": "fare-parent-bulk-001",
"child": {
"name": "200+ units (40% off)",
"amount": "60000",
"minQuantity": "200",
"status": "ACTIVATED",
"rules": [
{
"attribute": "quantity",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "200",
"priority": 1
}
]
}
}Response:
{
"id": "fare-child-004",
"parentId": "fare-parent-bulk-001",
"fareSetId": "fareset-laptop-001",
"name": "200+ units (40% off)",
"amount": "60000",
"minQuantity": "200",
"status": "ACTIVATED",
"rulesCount": 1,
"createdAt": "2026-02-25T10:30:00Z"
}Result: Parent's childrenCount is automatically incremented from 3 to 4.
10. Related Documentation
- Pricing Service — Package overview
- Tax System — Tax calculation
- Cost Tracking — Cost management
- Promotions — Promotion system
- Commerce Pricing — Conceptual pricing flow