Hệ thống Khuyến mãi
1. Tổng quan
Hệ thống khuyến mãi quản lý các chiến dịch khuyến mãi thông qua Promotions (định nghĩa chiến dịch với quy tắc điều kiện), PromotionMethods (phương pháp tính toán giảm giá với quy tắc nguồn và đích), và Rules (điều kiện ngữ cảnh cho điều kiện và nhắm mục tiêu). Nó hỗ trợ mã khuyến mãi thủ công, khuyến mãi tự động, giảm giá theo phần trăm/cố định, chiến dịch BuyGet và các chiến lược phân bổ đa dạng.
Mã nguồn: src/services/management/promotion.service.ts (CRUD), src/services/core/promotion-compute.service.ts-disable (tính toán - đã tắt) Controllers: src/controllers/promotion.controller.ts, promotion-method.controller.tsRoutes: /promotions, /promotion-methods
Trạng thái: ⚠️ Tính năng tính toán giảm giá chưa được triển khai. Promotion compute service (promotion-compute.service.ts) hiện đang bị tắt. Tất cả thao tác CRUD khuyến mãi hoạt động đúng, nhưng việc áp dụng giảm giá trong quá trình tính giá luôn trả về discount: '0'. Khuyến mãi phải được áp dụng và giảm giá phải được tính toán bên ngoài cho đến khi tính năng này được triển khai.
1.1. Tham chiếu Controller API
| Method | Path | Mô tả |
|---|---|---|
POST | /promotions | Tạo khuyến mãi |
GET | /promotions | Danh sách khuyến mãi |
GET | /promotions/:id | Lấy khuyến mãi theo ID |
PUT | /promotions/:id | Cập nhật khuyến mãi |
DELETE | /promotions/:id | Xóa khuyến mãi |
POST | /promotions/aggregate | Tạo khuyến mãi với method và rules trong một request |
PATCH | /promotions/:id/aggregate | Cập nhật khuyến mãi, method và rules trong một request |
POST | /promotion-methods | Tạo promotion method |
GET | /promotion-methods | Danh sách promotion methods |
GET | /promotion-methods/:id | Lấy promotion method theo ID |
PUT | /promotion-methods/:id | Cập nhật promotion method |
DELETE | /promotion-methods/:id | Xóa promotion method |
Aggregate endpoints (
/promotions/aggregate,/promotions/:id/aggregate) được tạo ra để tránh nhiều lần gọi API khi thiết lập khuyến mãi cùng với discount method và eligibility rules. Một lần gọi duy nhất tạo hoặc cập nhật tất cả mọi thứ theo cách nguyên tử trong một database transaction.
2. Mô hình dữ liệu
2.1. Loại khuyến mãi
| Loại | Mô tả | Trường hợp sử dụng |
|---|---|---|
| STANDARD | Giảm giá theo phần trăm hoặc cố định đơn giản | "Giảm 10% toàn bộ đơn hàng", "Giảm 50.000đ phí vận chuyển" |
| BUYGET | Mua X nhận Y có điều kiện | "Mua 2 tặng 1", "Mua 1.000.000đ giảm 20% phụ kiện" |
2.2. Vòng đời trạng thái khuyến mãi
| Trạng thái | Mô tả |
|---|---|
| DRAFT | Đã tạo nhưng chưa hoạt động — có thể chỉnh sửa tự do |
| ACTIVATED | Khuyến mãi đang hoạt động — có thể áp dụng cho đơn hàng |
| DEACTIVATED | Tạm dừng — có thể kích hoạt lại |
| EXPIRED | Qua ngày effectiveTo — chỉ đọc |
| ARCHIVED | Vô hiệu hóa vĩnh viễn — chỉ lưu trữ lịch sử |
2.3. Loại mục tiêu
| Loại | Áp dụng cho | Ví dụ |
|---|---|---|
| ITEMS | Các mục đơn hàng riêng lẻ khớp quy tắc đích | "Giảm 10% danh mục giày" |
| ORDER | Toàn bộ tổng phụ đơn hàng | "Giảm 15% đơn hàng trên 1.000.000đ" |
| SHIPPING | Chi phí vận chuyển | "Miễn phí vận chuyển cho đơn hàng trên 500.000đ" |
2.4. Chiến lược phân bổ
| Chiến lược | Hành vi | Ví dụ |
|---|---|---|
| EACH | Giảm giá áp dụng cho từng mục khớp riêng lẻ | "Giảm 10% mỗi mục trong Điện tử" → 3 mục = 3× giảm giá |
| ACROSS | Giảm giá phân bổ trên tất cả các mục khớp | "Giảm 100.000đ Điện tử" → 100.000đ chia cho tất cả các mục khớp |
| ONCE | Giảm giá áp dụng một lần bất kể số lượng | "Mua 2 tặng 1" → Áp dụng một lần cho mỗi bộ đủ điều kiện |
2.5. Quy ước ngữ cảnh quy tắc
Quy tắc được phân biệt bởi metadata.context:
| Ngữ cảnh | Gắn vào | Mục đích | Ví dụ |
|---|---|---|---|
| eligibility | Promotion | Ai có thể sử dụng khuyến mãi này? | Nhóm khách hàng = "VIP" |
| source | PromotionMethod | Phải mua gì? (Chỉ BuyGet) | Danh mục sản phẩm = "Laptop" VÀ số lượng >= 2 |
| target | PromotionMethod | Giảm giá gì? | Danh mục sản phẩm = "Phụ kiện" |
3. Luồng tạo khuyến mãi
3.1. Khuyến mãi đơn giản (CRUD)
POST /promotions
Content-Type: application/json
Authorization: Bearer <token>
{
"code": "SUMMER2026",
"name": { "en": "Summer Sale", "vi": "Khuyến mãi mùa hè" },
"type": "STANDARD",
"status": "DRAFT",
"isAutomatic": false,
"effectiveFrom": "2026-06-01T00:00:00Z",
"effectiveTo": "2026-08-31T23:59:59Z",
"usageLimit": 1000
}3.2. Khuyến mãi tổng hợp (Promotion + Method + Rules)
Tạo khuyến mãi hoàn chỉnh với phương pháp và tất cả quy tắc trong một giao dịch nguyên tử:
POST /promotions/aggregate
Content-Type: application/json
Authorization: Bearer <token>
{
"promotion": {
"code": "NEWUSER20",
"name": { "en": "New User 20% Off", "vi": "Người dùng mới giảm 20%" },
"type": "STANDARD",
"isAutomatic": false,
"effectiveFrom": "2026-03-01T00:00:00Z",
"usageLimit": 500
},
"promotionMethod": {
"targetType": "ORDER",
"allocation": "ONCE",
"type": "PERCENTAGE",
"value": "20",
"currency": "VND"
},
"promotionRules": [
{
"attribute": "customer.registeredAt",
"operator": "GTE",
"dataType": "TEXT",
"tValue": "2026-01-01T00:00:00Z",
"priority": 1,
"metadata": { "context": "eligibility" }
}
],
"sourceRules": [],
"targetRules": [
{
"attribute": "order.subtotal",
"operator": "GTE",
"dataType": "NUMBER",
"nValue": "500000",
"priority": 1,
"metadata": { "context": "target" }
}
]
}Đảm bảo giao dịch: Tất cả entity được tạo hoặc không có (rollback nếu có lỗi).
4. Áp dụng khuyến mãi
⚠️ Chưa hoạt động.
PromotionComputeService(promotion-compute.service.ts) hiện đang bị tắt (đổi tên thành.ts-disable). Các ví dụ dưới đây ghi lại hợp đồng API dự kiến; tính toán giảm giá thực tế chưa hoạt động.
4.1. Áp dụng thủ công
POST /promotions/apply
Content-Type: application/json
Authorization: Bearer <token>
{
"promotionCode": "SUMMER2026",
"context": {
"customer": {
"id": "customer-123",
"groupId": "vip"
},
"order": {
"subtotal": "1000000"
},
"items": [
{
"id": "item-1",
"productId": "prod-1",
"variantId": "var-1",
"quantity": 2,
"price": "500000"
}
]
}
}Phản hồi:
{
"isApplicable": true,
"discountAmount": "100000",
"itemDiscounts": [
{
"itemId": "item-1",
"discountAmount": "100000",
"reason": "SUMMER2026 - Giảm 10%"
}
]
}4.2. Khuyến mãi tự động
Khuyến mãi tự động (isAutomatic: true) được đánh giá trong quá trình thanh toán mà không yêu cầu mã khuyến mãi:
// Trong luồng thanh toán
const promotions = await promotionRepository.findAutomaticPromotions({
effectiveDate: new Date()
});
for (const promotion of promotions) {
const result = await promotionComputeService.applyPromotion({
promotion,
context: orderContext
});
if (result.isApplicable) {
// Áp dụng giảm giá vào đơn hàng
}
}5. Ví dụ khuyến mãi BuyGet
Kịch bản: "Mua 2 laptop, tặng 1 chuột"
{
"promotion": {
"code": "LAPTOP2GET1",
"type": "BUYGET",
"name": { "en": "Buy 2 Laptops Get 1 Mouse Free", "vi": "Mua 2 Laptop tặng 1 chuột" }
},
"promotionMethod": {
"targetType": "ITEMS",
"allocation": "ONCE",
"type": "PERCENTAGE",
"value": "100",
"buyGetSourceMinQuantity": 2,
"buyGetTargetQuantity": 1
},
"sourceRules": [
{
"attribute": "product.categoryId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "laptops",
"metadata": { "context": "source" }
}
],
"targetRules": [
{
"attribute": "product.categoryId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "accessories",
"metadata": { "context": "target" }
},
{
"attribute": "product.subcategoryId",
"operator": "EQ",
"dataType": "TEXT",
"tValue": "mouse",
"metadata": { "context": "target" }
}
]
}Logic:
- Đánh giá quy tắc nguồn: Tìm các mục có
categoryId = "laptops"VÀsum(quantity) >= 2 - Nếu nguồn đạt, đánh giá quy tắc đích: Tìm các mục có
categoryId = "accessories"VÀsubcategoryId = "mouse" - Áp dụng giảm giá 100% cho chuột giá thấp nhất (lên đến
buyGetTargetQuantity = 1)
6. Thao tác quản lý khuyến mãi
6.1. Kích hoạt khuyến mãi
PUT /promotions/:id/activate
Authorization: Bearer <token>Thay đổi trạng thái từ DRAFT → ACTIVATED. Xác thực rằng khuyến mãi có phương pháp hợp lệ đính kèm.
6.2. Vô hiệu hóa khuyến mãi
PUT /promotions/:id/deactivate
Authorization: Bearer <token>Thay đổi trạng thái từ ACTIVATED → DEACTIVATED. Khuyến mãi có thể được kích hoạt lại sau.
6.3. Xóa khuyến mãi (Cascade)
DELETE /promotions/:id
Authorization: Bearer <token>Soft-delete:
- Entity Promotion
- PromotionMethod liên kết
- Tất cả Rules đính kèm (eligibility, source, target)
Tất cả các xóa đều có giao dịch (rollback nếu có lỗi).
7. Thao tác Repository
7.1. Promotion Repository
| Phương thức | Mô tả |
|---|---|
findByCode() | Tìm khuyến mãi đang hoạt động theo mã (loại trừ đã xóa, lọc theo thời gian) |
findAutomaticPromotions() | Lấy tất cả khuyến mãi tự động áp dụng đang hoạt động tại ngày cho trước |
findActivePromotions() | Lấy tất cả khuyến mãi đang hoạt động trong phạm vi ngày |
isUsageLimitReached() | Kiểm tra nếu khuyến mãi đã đạt giới hạn sử dụng |
incrementUsageCount() | Tăng nguyên tử của usageCount (raw SQL) |
updateRulesCount() | Cập nhật rulesCount denormalized |
findByStatus() | Lọc khuyến mãi theo trạng thái |
7.2. PromotionMethod Repository
| Phương thức | Mô tả |
|---|---|
findByPromotionId() | Lấy phương pháp cho khuyến mãi (quan hệ 1:1) |
updateSourceRulesCount() | Cập nhật sourceRulesCount denormalized |
updateTargetRulesCount() | Cập nhật targetRulesCount denormalized |
7.3. Rule Repository (Đặc biệt cho khuyến mãi)
| Phương thức | Mô tả |
|---|---|
findEligibilityRulesByPromotionId() | Lấy quy tắc điều kiện (metadata.context = 'eligibility') |
findSourceRulesByMethodId() | Lấy quy tắc nguồn BuyGet (metadata.context = 'source') |
findTargetRulesByMethodId() | Lấy quy tắc đích (metadata.context = 'target') |
deleteByPromotionId() | Cascade delete tất cả quy tắc điều kiện khuyến mãi |
deleteByMethodId() | Cascade delete tất cả quy tắc phương pháp (source + target) |
8. Tích hợp với luồng định giá
8.1. Trạng thái hiện tại (Đã tắt)
Trong PricingService.calculate(), giảm giá được hardcode vì compute service đang bị tắt:
// Hardcode — PromotionComputeService đã bị tắt
discount: '0', // TODO: Áp dụng khuyến mãi tại đây8.2. Triển khai dự kiến
// Triển khai tương lai
const applicablePromotions = await this.promotionComputeService.applyAutomaticPromotions({
context: pricingContext
});
const discount = applicablePromotions.reduce((sum, promo) =>
sum + parseFloat(promo.discountAmount), 0
);9. Các quyết định thiết kế chính
| Quyết định | Lý do |
|---|---|
| Hệ thống quy tắc đa hình | Bảng Rule đơn phục vụ Fare, Tax và Promotion — giảm độ phức tạp schema |
| Metadata ngữ cảnh quy tắc | Sử dụng quy ước metadata.context thay vì các trường riêng biệt — linh hoạt và có thể mở rộng |
| 1:1 Promotion:Method | Đơn giản hóa mô hình so với mảng ApplicationMethod của Medusa.js — đủ cho nhu cầu hiện tại |
| Số lượng denormalized | rulesCount, sourceRulesCount, targetRulesCount — tránh joins cho các truy vấn phổ biến |
| Hỗ trợ i18n | Tất cả văn bản hướng người dùng (name, description) được lưu trữ dưới dạng JSONB — sẵn sàng đa ngôn ngữ |
| Vòng đời trạng thái | 5 trạng thái (DRAFT → ACTIVATED → DEACTIVATED → EXPIRED → ARCHIVED) — máy trạng thái rõ ràng |
| Tạo aggregate nguyên tử | Giao dịch đơn cho Promotion + Method + Rules — tính nhất quán dữ liệu |
| Service tính toán riêng | Logic tính toán tách biệt khỏi CRUD — tách biệt mối quan tâm |
10. Trạng thái triển khai
| Thành phần | Trạng thái | Ghi chú |
|---|---|---|
| Schemas | ✅ Hoàn thành | Promotion, PromotionMethod, Rule (đa hình) |
| Repositories | ✅ Hoàn thành | Mở rộng với các phương thức đặc biệt cho khuyến mãi |
| Controllers | ✅ CRUD cơ bản | Sử dụng mẫu ControllerFactory |
| Management Service | ✅ Hoàn thành | PromotionService với 6 phương thức (CRUD, activate, deactivate, delete) |
| Compute Service | ❌ Đã tắt | promotion-compute.service.ts đổi tên thành .ts-disable — bị loại khỏi build có chủ ý |
| Tích hợp định giá | ❌ TODO | Chưa tích hợp vào luồng định giá động |
| Request Models | ✅ Hoàn thành | Create, update, aggregate, apply requests |
| Response Models | ✅ Hoàn thành | Aggregate và application responses |
11. Tài liệu liên quan
- Dịch vụ Định giá — Tổng quan package và kiến trúc
- Hệ thống Giá vé — Mẫu đánh giá quy tắc (chia sẻ với khuyến mãi)
- Hệ thống Thuế — Luồng tính toán thuế
- Sale Service — Quản lý đơn hàng và tích hợp thanh toán
- Core Database Schema — Định nghĩa bảng khuyến mãi