Hệ thống Giá vé
1. Tổng quan
Hệ thống giá vé quản lý định giá sản phẩm thông qua FareSets (bộ chứa cho mỗi biến thể sản phẩm), Fares (các mục giá được tổ chức theo phân cấp cha/con), và Rules (điều kiện ngữ cảnh gắn vào giá vé con). Nó hỗ trợ giá gốc tĩnh, giảm giá theo số lượng, định giá theo thời gian, và ghi đè theo kênh bán.
Source: src/services/core/pricing-fare-calculator.service.ts, src/services/management/fare.service.tsController: src/controllers/fare.controller.tsRoute: /fares, /fare-sets, /rules
2. Mô hình Dữ liệu
2.1. Các loại Giá vé
| 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 — giá vé con hợp lệ đầu tiên được chọn ngay lập tức |
DISCOUNT | Các giá vé con cạnh tranh — giá thấp nhất trong các con hợp lệ được chọn |
3. Luồng Chọn Giá vé
3.1. Kết quả Chọn lọc
type TFareSelectionResult = {
fareSet: TFareSet;
selectedFare: TFare; // Giá vé được chọn
baseFare: TFare; // Giá vé 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 | Giá vé được thuật toán chọn cuối cùng |
baseFare | TFare | Giá vé 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 giá vé 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 giá vé, fare repository lọc các giá vé đ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 giá vé vượt qua cả hai bộ lọc mới được xét trong bước đánh giá quy tắc. Điều này thu hẹp tập ứng viên trước khi bất kỳ logic quy tắc 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. Nhóm Giá vé
Nhóm giá vé là một giá vé cha với một hoặc nhiều giá vé con. Mỗi giá vé con có thể có các quy tắc xác định khi nào nó được áp dụng.
4.1. Tạo Nhóm Giá vé
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 giá vé cha | fareSetId, type, status — không có amount |
| 2 | Tạo giá vé con | Cho mỗi phần tử con trong mảng |
| 3 | Tạo quy tắc | Tạo hàng loạt quy tắc với principalId=child.id, principalType='Fare' |
| 4 | Cập nhật rulesCount | Đặt fare.rulesCount cho mỗi giá vé con |
| 5 | Cập nhật childrenCount | Đếm và đặt parent.childrenCount |
4.3. Thêm Con 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á Quy tắc
Quy tắc được đánh giá với logic AND — tất cả quy tắc trên một giá vé con phải thỏa mãn để giá vé con đó 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 | Thuộc tập hợp | saleChannelId IN ["ch-1", "ch-2"] |
NIN | Không thuộc tập hợp | merchantId NIN ["blocked-1"] |
5.2. Kiểu Dữ liệu
| Kiểu | Trường | Trường hợp sử dụng |
|---|---|---|
TEXT | tValue | Khớp channel ID, merchant ID, thuộc tính chuỗi |
NUMBER | nValue | Ngưỡng số lượng, phạm vi giá |
BOOLEAN | bValue | Cờ tính năng, 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
Bộ đánh giá quy tắc trích xuất giá trị từ ngữ cảnh định giá sử 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 Biến thể Sản phẩm
Khi một biến thể sản phẩm được tạo, hệ thống giá vé tự động thiết lập định giá:
7. API Controller
Lưu ý: Tính toán giá (tĩnh và động) hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm chi tiết.
FareController
| Phương thức | Đường dẫn | Mô tả |
|---|---|---|
POST | /fares | Tạo giá vé |
GET | /fares | Danh sách giá vé |
GET | /fares/count | Đếm giá vé |
GET | /fares/find-one | Tìm một giá vé |
GET | /fares/:id | Lấy giá vé theo ID |
PUT | /fares/:id | Cập nhật giá vé |
DELETE | /fares/:id | Xóa giá vé |
POST | /fares/groups | Tạo nhóm giá vé (cha + con) |
POST | /fares/children | Thêm giá vé con vào nhóm cha hiện có |
FareSetController
| Phương thức | Đường dẫn | Mô tả |
|---|---|---|
POST | /fare-sets | Tạo fare set |
GET | /fare-sets | Danh sách fare set |
GET | /fare-sets/:id | Lấy fare set theo ID |
PUT | /fare-sets/:id | Cập nhật fare set |
DELETE | /fare-sets/:id | Xóa fare set |
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 Nhóm Giá vé
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 giá qua Simulation
Lưu ý: Tính toán giá hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm 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á Theo Thời gian
Quy tắc Kinh doanh: "Giờ sáng sớm (6-9 AM) = giảm 20%, Giờ cao điểm (12-2 PM) = tăng 30%, Đêm muộn (10 PM - 1 AM) = giảm 15%"
Bước 1: Tạo Nhóm Giá vé 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 giá qua Simulation
Lưu ý: Tính toán giá hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm 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 Bán
Quy tắc Kinh doanh: "Online = giá gốc, Quầy kiosk = +10%, Đặt qua điện thoại = +15%, Kênh đối tác = -5%"
Bước 1: Tạo Nhóm Giá vé 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 giá qua Simulation
Lưu ý: Tính toán giá hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm 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 Đa Quy tắc (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 nhận giá đặc biệt"
Bước 1: Tạo Nhóm Giá vé 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 giá qua Simulation (Đáp ứng Tất cả Điều kiện)
Lưu ý: Tính toán giá hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm chi tiết.
Ví dụ kết quả chọn lọc (tất cả quy tắc thỏa mãn — 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 giá qua Simulation (Một Quy tắc Thất bại — Thứ Bảy)
Khi
dayOfWeeklà"Saturday", quy tắc ngày trong tuần thất bại. Simulation quay về giá mặc định.
Ví dụ kết quả chọn lọc (quay về mặc định — quy tắc dayOfWeek thất bại):
{
"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 Khoảng Ngày
Quy tắc Kinh doanh: "Giảm giá mùa hè (Tháng 6-8): Giảm 25% tất cả đơn hàng"
Bước 1: Tạo Giá vé 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 giá qua Simulation (Trong Chiến dịch)
Lưu ý: Tính toán giá hiện được thực hiện qua
POST /simulation/calculate. Xem Simulation Endpoint để biết thêm 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 giá qua Simulation (Sau Chiến dịch)
Khi
effectiveDatelà"2026-09-01", bộ lọceffectiveTocủa giá vé loại nó khỏi tập ứng viên. Simulation quay về giá mặc định.
Ví dụ kết quả chọn lọc (chiến dịch hết hạn — quay về mặc định):
{
"selectedFare": {
"id": "fare-default-001",
"amount": "100000"
},
"baseFare": { "amount": "100000" },
"appliedRules": [],
"selectionReason": "default"
}9.6. Kịch bản 6: Thêm Giá vé Con vào Nhóm Hiện có
Quy tắc Kinh doanh: "Thêm bậc mới vào giảm giá số lượng lớn hiện có: 200+ sản phẩm = giảm 40%"
Bước 1: Thêm Giá vé Con vào Nhóm Cha
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 nhóm cha tự động tăng từ 3 lên 4.
10. Tài liệu Liên quan
- Dịch vụ Định giá — Tổng quan package
- Hệ thống Thuế — Tính toán thuế
- Theo dõi Chi phí — Quản lý chi phí
- Khuyến mãi — Hệ thống khuyến mãi
- Định giá Commerce — Luồng định giá khái niệm