Fare System
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| Status | Ổn định (input định giá canonical) |
| Owner | pricing-team |
| Services | FareService (CRUD), PricingFareCalculatorService (v1), FareCalculatorService (v2), PricingRuleEvaluatorService |
| Controllers / Routes | FareController /fares, FareSetController /fare-sets, RuleController /rules |
| Depends on | schema 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ại | parentId | childrenCount | type | amount | Vai trò |
|---|---|---|---|---|---|
| Default | null | null | null | set | Giá gốc - dự phòng khi không có quy tắc nào khớp |
| Parent | null | > 0 | OVERRIDE / DISCOUNT | null | Bộ chứa nhóm - định nghĩa chiến lược chọn |
| Child | set | null | null | set | Giá biến thể - được chọn khi quy tắc thỏa mãn |
2.2. Loại Parent
| Loại | Hà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 |
DISCOUNT | Cá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
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ường | Kiểu | Mô tả |
|---|---|---|
fareSet | TFareSet | FareSet đang kích hoạt cho biến thể sản phẩm |
selectedFare | TFare | Fare được thuật toán chọn cuối cùng |
baseFare | TFare | Fare mặc định (hữu ích để so sánh chiết khấu) |
appliedRules | TRule[] | 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ện | Ghi chú |
|---|---|---|
effectiveDate | effectiveFrom <= date <= effectiveTo | effectiveTo = null nghĩa là không hết hạn |
quantity | minQuantity <= qty <= maxQuantity | Giớ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/maxQuantitytrê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
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ước | Hành động | Chi tiết |
|---|---|---|
| 1 | Tạo parent fare | fareSetId, type, status - không amount |
| 2 | Tạo child fares | Cho mỗi child trong mảng |
| 3 | Tạo rules | Batch tạo rule với principalId=child.id, principalType='Fare' |
| 4 | Cập nhật rulesCount | Đặt fare.rulesCount cho mỗi child |
| 5 | Cập nhật childrenCount | Đếm và đặt parent.childrenCount |
4.3. Thêm Child vào Nhóm Hiện có
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ụ |
|---|---|---|
EQ | Bằng | merchantId EQ "m-123" |
NE / NEQ | Không bằng | status NEQ "blocked" |
GT | Lớn hơn | quantity GT 5 |
GTE | Lớn hơn hoặc bằng | quantity GTE 10 |
LT | Nhỏ hơn | quantity LT 100 |
LTE | Nhỏ hơn hoặc bằng | quantity LTE 50 |
IN / INQ | Trong tập | saleChannelId IN ["ch-1", "ch-2"] |
NIN | Không trong tập | merchantId NIN ["blocked-1"] |
5.2. Kiểu Dữ liệu
| Kiểu | Trường | Trường hợp dùng |
|---|---|---|
TEXT | tValue | Khớp channel ID, merchant ID, thuộc tính chuỗi |
NUMBER | nValue | Ngưỡng số lượng, khoảng giá |
BOOLEAN | bValue | Feature flag, trạng thái thành viên |
JSON | jValue | Đ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():
// Rule: { attribute: "quantity", operator: "GTE", nValue: "10" }
// Context: { quantity: "15", merchantId: "m-123", saleChannelId: "ch-1" }
// → get(context, "quantity") = "15" → 15 >= 10 → PASS6. 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ỉnhPOST /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
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:
{
"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
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:
{
"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
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:
{
"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
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):
{
"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
dayOfWeeklà"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):
{
"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
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:
{
"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
effectiveDatelà"2026-09-01", bộ lọceffectiveTocủ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):
{
"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ó
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:
{
"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
- Pricing Service - Tổng quan package
- Tax System - Tính toán thuế
- Cost Tracking - Quản lý chi phí
- Promotions - Hệ thống khuyến mãi
- Commerce Pricing - Luồng định giá khái niệm