Skip to content

Fare System

1. Tổng quan

Thuộc tínhGiá trị
StatusỔn định (input định giá canonical)
Ownerpricing-team
ServicesFareService (CRUD), PricingFareCalculatorService (v1), FareCalculatorService (v2), PricingRuleEvaluatorService
Controllers / RoutesFareController /fares, FareSetController /fare-sets, RuleController /rules
Depends onschema FareSet, Fare, Rule (@nx/core)

Quản lý định giá thông qua FareSet (một bộ chứa cho mỗi product variant), Fare (các mục giá theo phân cấp cha/con), và Rule (điều kiện ngữ cảnh trên child fare). Hỗ trợ giá tĩnh, giảm giá theo số lượng, định giá theo thời gian, và ghi đè theo kênh/FBT.

Danh mục service và identity card: xem Pricing Service index. Tham chiếu REST: trực tiếp tại /v1/api/pricing/doc/openapi.json.

2. Mô hình Dữ liệu

2.1. Các loại Fare

LoạiparentIdchildrenCounttypeamountVai trò
DefaultnullnullnullsetGiá gốc - dự phòng khi không có quy tắc nào khớp
Parentnull> 0OVERRIDE / DISCOUNTnullBộ chứa nhóm - định nghĩa chiến lược chọn
ChildsetnullnullsetGiá biến thể - được chọn khi quy tắc thỏa mãn

2.2. Loại Parent

LoạiHành vi
OVERRIDEƯu tiên cao nhất - child fare hợp lệ đầu tiên được chọn ngay lập tức
DISCOUNTCác child fare cạnh tranh - giá thấp nhất trong các con hợp lệ được chọn

3. Luồng Chọn Fare

3.1. Kết quả Chọn lọc

typescript
type TFareSelectionResult = {
  fareSet: TFareSet;
  selectedFare: TFare;       // Fare được chọn
  baseFare: TFare;           // Fare mặc định (giống fallback fare)
  appliedRules: TRule[];     // Các quy tắc đã khớp
  selectionReason: 'default' | 'override' | 'discount';
};
TrườngKiểuMô tả
fareSetTFareSetFareSet đang kích hoạt cho biến thể sản phẩm
selectedFareTFareFare được thuật toán chọn cuối cùng
baseFareTFareFare mặc định (hữu ích để so sánh chiết khấu)
appliedRulesTRule[]Các quy tắc đánh giá đúng cho child được chọn
selectionReason'default' | 'override' | 'discount'Lý do fare này được chọn

3.2. Lọc trước theo Ngày và Số lượng

Trước khi bắt đầu chọn fare, fare repository lọc các fare đang hoạt động theo hai tiêu chí:

Bộ lọcĐiều kiệnGhi chú
effectiveDateeffectiveFrom <= date <= effectiveToeffectiveTo = null nghĩa là không hết hạn
quantityminQuantity <= qty <= maxQuantityGiới hạn null được coi là không giới hạn

Chỉ những fare vượt qua cả hai bộ lọc mới được xét trong bước đánh giá rule. Điều này thu hẹp tập ứng viên trước khi bất kỳ logic rule nào chạy.

Mẹo: Sử dụng minQuantity/maxQuantity trên child fare để đảm bảo chúng không bao giờ được tải khi yêu cầu nằm ngoài phạm vi số lượng dự kiến - hiệu quả hơn việc chỉ dựa vào toán tử quy tắc.

4. Fare Group

Một fare group là một parent fare với một hoặc nhiều child fare. Mỗi child có thể có rule quyết định khi nào nó áp dụng.

4.1. Tạo 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. Các bước Tạo

BướcHành độngChi tiết
1Tạo parent farefareSetId, type, status - không amount
2Tạo child faresCho mỗi child trong mảng
3Tạo rulesBatch tạo rule với principalId=child.id, principalType='Fare'
4Cập nhật rulesCountĐặt fare.rulesCount cho mỗi child
5Cập nhật childrenCountĐếm và đặt parent.childrenCount

4.3. Thêm Child vào Nhóm Hiện có

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. Đánh giá Rule

Rule được đánh giá với logic AND - mọi rule trên một child fare phải pass thì child đó mới hợp lệ.

5.1. Toán tử

Toán tửMô tảVí dụ
EQBằngmerchantId EQ "m-123"
NE / NEQKhông bằngstatus NEQ "blocked"
GTLớn hơnquantity GT 5
GTELớn hơn hoặc bằngquantity GTE 10
LTNhỏ hơnquantity LT 100
LTENhỏ hơn hoặc bằngquantity LTE 50
IN / INQTrong tậpsaleChannelId IN ["ch-1", "ch-2"]
NINKhông trong tậpmerchantId NIN ["blocked-1"]

5.2. Kiểu Dữ liệu

KiểuTrườngTrường hợp dùng
TEXTtValueKhớp channel ID, merchant ID, thuộc tính chuỗi
NUMBERnValueNgưỡng số lượng, khoảng giá
BOOLEANbValueFeature flag, trạng thái thành viên
JSONjValueĐiều kiện lồng phức tạp

5.3. Khớp Ngữ cảnh

Rule evaluator trích giá trị từ pricing context dùng 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. Tích hợp Product Variant

Khi một product variant được tạo, fare system tự động thiết lập việc định giá:

7. API Controller

Tính toán định giá chạy qua POST /simulation/calculate (v1) và POST /simulation-v2/calculate (v2). Quản lý fare chạy qua:

  • FareController /fares - CRUD + tuỳ chỉnh POST /fares/groups (parent + children) và POST /fares/children (thêm child vào parent hiện có).
  • FareSetController /fare-sets - CRUD.
  • RuleController /rules - CRUD.

Tham chiếu request/response đầy đủ được render trực tiếp từ /v1/api/pricing/doc/openapi.json - không bảo trì thủ công ở đây.


8. Phụ thuộc Service

9. Ví dụ Thực tế

9.1. Kịch bản 1: Giảm giá Số lượng Lớn

Quy tắc Kinh doanh: "Mua 10+ sản phẩm giảm 10%, 50+ giảm 20%, 100+ giảm 30%"

Bước 1: Tạo 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
        }
      ]
    }
  ]
}

Bước 2: Tính Định giá qua Simulation

Lưu ý: Tính toán định giá hiện được thực hiện qua POST /simulation/calculate. Xem Simulation Endpoint để biết chi tiết.

Ví dụ kết quả chọn lọc:

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. Kịch bản 2: Định giá Động theo Thời gian

Quy tắc Kinh doanh: "Early bird (6-9 sáng) = giảm 20%, Giờ cao điểm (12-2 chiều) = phụ phí 30%, Đêm khuya (10 tối - 1 sáng) = giảm 15%"

Bước 1: Tạo Fare Group theo Thời gian

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
        }
      ]
    }
  ]
}

Bước 2: Tính Định giá qua Simulation

Lưu ý: Tính toán định giá hiện được thực hiện qua POST /simulation/calculate. Xem Simulation Endpoint để biết chi tiết.

Ví dụ kết quả chọn lọc:

json
{
  "selectedFare": {
    "id": "fare-peak-001",
    "name": "Peak Hours Premium",
    "amount": "130000"
  },
  "baseFare": { "amount": "100000" },
  "selectionReason": "override"
}

9.3. Kịch bản 3: Định giá theo Kênh

Quy tắc Kinh doanh: "Online = giá gốc, Kiosk tại cửa hàng = +10%, Đặt qua điện thoại = +15%, Kênh đối tác = -5%"

Bước 1: Tạo Fare Group theo Kênh

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
        }
      ]
    }
  ]
}

Bước 2: Tính Định giá qua Simulation

Lưu ý: Tính toán định giá hiện được thực hiện qua POST /simulation/calculate. Xem Simulation Endpoint để biết chi tiết.

Ví dụ kết quả chọn lọc:

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. Kịch bản 4: Kết hợp Nhiều Rule (Số lượng + Thời gian + Kênh)

Quy tắc Kinh doanh: "Khách hàng kênh VIP mua 20+ sản phẩm vào buổi sáng ngày thường được giá đặc biệt"

Bước 1: Tạo Fare Group Phức tạp

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
        }
      ]
    }
  ]
}

Bước 2: Tính Định giá qua Simulation (Mọi Điều kiện Thỏa mãn)

Lưu ý: Tính toán định giá hiện được thực hiện qua POST /simulation/calculate. Xem Simulation Endpoint để biết chi tiết.

Ví dụ kết quả chọn lọc (mọi rule pass - logic AND):

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"
}

Bước 3: Tính Định giá qua Simulation (Một Rule Fail - Thứ Bảy)

Khi dayOfWeek"Saturday", rule ngày thường fail. Simulation fallback về default fare.

Ví dụ kết quả chọn lọc (fallback về default - rule dayOfWeek fail):

json
{
  "selectedFare": {
    "id": "fare-default-001",
    "amount": "100000"
  },
  "baseFare": { "amount": "100000" },
  "appliedRules": [],
  "selectionReason": "default"
}

9.5. Kịch bản 5: Chiến dịch Theo mùa với Phạm vi Ngày

Quy tắc Kinh doanh: "Sale mùa hè (Tháng 6-8): giảm 25% mọi giao dịch"

Bước 1: Tạo Fare Theo mùa

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
        }
      ]
    }
  ]
}

Bước 2: Tính Định giá qua Simulation (Trong Chiến dịch)

Lưu ý: Tính toán định giá hiện được thực hiện qua POST /simulation/calculate. Xem Simulation Endpoint để biết chi tiết.

Ví dụ kết quả chọn lọc:

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"
}

Bước 3: Tính Định giá qua Simulation (Sau Chiến dịch)

Khi effectiveDate"2026-09-01", bộ lọc effectiveTo của fare loại nó khỏi tập ứng viên. Simulation fallback về default fare.

Ví dụ kết quả chọn lọc (chiến dịch hết hạn - fallback về default):

json
{
  "selectedFare": {
    "id": "fare-default-001",
    "amount": "100000"
  },
  "baseFare": { "amount": "100000" },
  "appliedRules": [],
  "selectionReason": "default"
}

9.6. Kịch bản 6: Thêm Child Fare vào Nhóm Hiện có

Quy tắc Kinh doanh: "Thêm một tier mới vào bulk discount hiện có: 200+ sản phẩm = giảm 40%"

Bước 1: Thêm Child vào Parent Hiện có

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
      }
    ]
  }
}

Phản hồi:

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"
}

Kết quả: childrenCount của parent tự động tăng từ 3 lên 4.


10. Tài liệu Liên quan

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