Skip to content

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ạ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 — giá vé con hợp lệ đầu tiên được chọn ngay lập tức
DISCOUNTCá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

typescript
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ườngKiểuMô tả
fareSetTFareSetFareSet đang kích hoạt cho biến thể sản phẩm
selectedFareTFareGiá vé được thuật toán chọn cuối cùng
baseFareTFareGiá vé 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 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ệ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 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/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. 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é

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 giá vé chafareSetId, type, status — không có amount
2Tạo giá vé conCho mỗi phần tử con trong mảng
3Tạo quy tắcTạo hàng loạt quy tắc với principalId=child.id, principalType='Fare'
4Cập nhật rulesCountĐặt fare.rulesCount cho mỗi giá vé con
5Cập nhật childrenCountĐếm và đặt parent.childrenCount

4.3. Thêm Con 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á 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ụ
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 / INQThuộc tập hợpsaleChannelId IN ["ch-1", "ch-2"]
NINKhông thuộc tập hợpmerchantId NIN ["blocked-1"]

5.2. Kiểu Dữ liệu

KiểuTrườngTrường hợp sử dụng
TEXTtValueKhớp channel ID, merchant ID, thuộc tính chuỗi
NUMBERnValueNgưỡng số lượng, phạm vi giá
BOOLEANbValueCờ tính năng, 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

Bộ đánh giá quy tắc trích xuất giá trị từ ngữ cảnh định giá sử 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 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ẫnMô tả
POST/faresTạo giá vé
GET/faresDanh sách giá vé
GET/fares/countĐếm giá vé
GET/fares/find-oneTìm một giá vé
GET/fares/:idLấy giá vé theo ID
PUT/fares/:idCập nhật giá vé
DELETE/fares/:idXóa giá vé
POST/fares/groupsTạo nhóm giá vé (cha + con)
POST/fares/childrenThêm giá vé con vào nhóm cha hiện có

FareSetController

Phương thứcĐường dẫnMô tả
POST/fare-setsTạo fare set
GET/fare-setsDanh sách fare set
GET/fare-sets/:idLấy fare set theo ID
PUT/fare-sets/:idCập nhật fare set
DELETE/fare-sets/:idXó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é

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

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á 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

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

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

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

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 Đ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

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

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 giá qua Simulation (Một Quy tắc Thất bại — Thứ Bảy)

Khi dayOfWeek"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):

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

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

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 giá qua Simulation (Sau Chiến dịch)

Khi effectiveDate"2026-09-01", bộ lọc effectiveTo củ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):

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

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 nhóm cha 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.