Skip to content

Fare System

1. Overview

PropertyValue
StatusStable (canonical pricing input)
Ownerpricing-team
ServicesFareService (CRUD), PricingFareCalculatorService (v1), FareCalculatorService (v2), PricingRuleEvaluatorService
Controllers / RoutesFareController /fares, FareSetController /fare-sets, RuleController /rules
Depends onFareSet, 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

TypeparentIdchildrenCounttypeamountRole
DefaultnullnullnullsetBase price — fallback when no rules match
Parentnull> 0OVERRIDE / DISCOUNTnullGroup container — defines selection strategy
ChildsetnullnullsetVariant price — selected when rules pass

2.2. Parent Types

TypeBehavior
OVERRIDEHighest priority — first valid child is selected immediately
DISCOUNTChildren compete — lowest price among valid children is selected

3. Fare Selection Flow

3.1. Selection Result

typescript
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';
};
FieldTypeDescription
fareSetTFareSetThe activated fare set for the product variant
selectedFareTFareThe fare ultimately chosen by the algorithm
baseFareTFareThe default fare (useful for discount comparison)
appliedRulesTRule[]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:

FilterConditionNotes
effectiveDateeffectiveFrom <= date <= effectiveToeffectiveTo = null means no expiry
quantityminQuantity <= qty <= maxQuantitynull 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/maxQuantity on 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

http
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

StepActionDetails
1Create parent farefareSetId, type, status — no amount
2Create child faresFor each child in array
3Create rulesBatch create rules with principalId=child.id, principalType='Fare'
4Update rulesCountSet fare.rulesCount for each child
5Update childrenCountCount and set parent.childrenCount

4.3. Add Child to Existing Group

http
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

OperatorDescriptionExample
EQEqualmerchantId EQ "m-123"
NE / NEQNot equalstatus NEQ "blocked"
GTGreater thanquantity GT 5
GTEGreater than or equalquantity GTE 10
LTLess thanquantity LT 100
LTELess than or equalquantity LTE 50
IN / INQIn setsaleChannelId IN ["ch-1", "ch-2"]
NINNot in setmerchantId NIN ["blocked-1"]

5.2. Data Types

TypeFieldUse Case
TEXTtValueMatch channel IDs, merchant IDs, string attributes
NUMBERnValueQuantity thresholds, price ranges
BOOLEANbValueFeature flags, membership status
JSONjValueComplex nested conditions

5.3. Context Matching

The rule evaluator extracts values from the pricing context using lodash get():

typescript
// Rule: { attribute: "quantity", operator: "GTE", nValue: "10" }
// Context: { quantity: "15", merchantId: "m-123", saleChannelId: "ch-1" }
// → get(context, "quantity") = "15" → 15 >= 10 → PASS

6. 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 + custom POST /fares/groups (parent + children) and POST /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

http
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:

json
{
  "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

http
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:

json
{
  "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

http
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:

json
{
  "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

http
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):

json
{
  "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 dayOfWeek is "Saturday", the weekday rule fails. The simulation falls back to the default fare.

Example selection result (falls back to default — dayOfWeek rule failed):

json
{
  "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

http
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:

json
{
  "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 effectiveDate is "2026-09-01", the fare's effectiveTo filter excludes it from the candidate pool. The simulation falls back to the default fare.

Example selection result (campaign expired — falls back to default):

json
{
  "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

http
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:

json
{
  "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.


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