Hệ thống Khuyến mãi
1. Tổng quan
| Thuộc tính | Giá trị |
|---|---|
| Status | Beta - chỉ CRUD; tính toán giảm giá chưa wire vào pipeline nào |
| Owner | pricing-team |
| Services | PromotionService (CRUD); promotion-compute.service.ts-disable (tính toán, đã tắt) |
| Controllers / Routes | PromotionController /promotions (POST /aggregate, PATCH /{id}/aggregate), PromotionMethodController /promotion-methods |
| Depends on | schema Promotion, PromotionMethod, Rule (@nx/core) |
Quản lý các chiến dịch khuyến mãi thông qua Promotions (chiến dịch + rule điều kiện), PromotionMethods (phương pháp giảm giá với rule nguồn/đích), và Rules (điều kiện ngữ cảnh). Hỗ trợ mã thủ công, khuyến mãi tự động, giảm giá phần trăm/cố định, chiến dịch BuyGet, và các chiến lược đa phân bổ.
⚠️ Tính toán giảm giá chưa được triển khai. Compute service đã tắt; cả v1 và v2 đều không áp dụng khuyến mãi, nên endpoint calculate trả về
discount: '0'. CRUD hoạt động đầy đủ. Giảm giá phải được áp dụng bên ngoài cho đến khi calculator hoàn thành.
Endpoint aggregate tạo/cập nhật một promotion cùng method và rules của nó một cách liền mạch trong một transaction (một round-trip). 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. Loại Khuyến mãi
| Loại | Mô tả | Trường hợp dùng |
|---|---|---|
| STANDARD | Giảm giá phần trăm hoặc cố định đơn giản | "Giảm 10% toàn order", "Giảm $5 phí ship" |
| BUY_GET | Mua X tặng Y có điều kiện | "Mua 2 tặng 1", "Mua $100 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 active - chỉnh tự do |
| ACTIVATED | Khuyến mãi đang chạy - áp được vào order |
| DEACTIVATED | Tạm dừng - có thể kích hoạt lại |
| EXPIRED | Quá ngày effectiveTo - chỉ đọc |
| ARCHIVED | Vô hiệu vĩnh viễn - chỉ là bản ghi lịch sử |
2.3. Loại Target
| Loại | Áp dụng cho | Ví dụ |
|---|---|---|
| ITEMS | Từng item order khớp rule target | "Giảm 10% danh mục giày" |
| ORDER | Tổng phụ toàn order | "Giảm 15% order trên $100" |
| SHIPPING | Chi phí ship | "Miễn phí ship cho order trên $50" |
2.4. Chiến lược Phân bổ
| Chiến lược | Hành vi | Ví dụ |
|---|---|---|
| EACH | Giảm giá áp lên từng item khớp riêng lẻ | "Giảm 10% mỗi item Electronics" → 3 item = 3× giảm |
| ACROSS | Giảm giá phân bổ trên mọi item khớp | "Giảm $10 Electronics" → $10 chia cho mọi item khớp |
| ONCE | Giảm giá áp một lần bất kể số lượng | "Mua 2 tặng 1" → Áp một lần mỗi tập đủ điều kiện |
2.5. Quy ước Ngữ cảnh Rule
Rule được phân biệt bởi metadata.context:
| Context | Gắn vào | Mục đích | Ví dụ |
|---|---|---|---|
| eligibility | Promotion | Ai dùng được khuyến mãi này? | Nhóm khách = "VIP" |
| source | PromotionMethod | Phải mua gì? (chỉ BuyGet) | Danh mục sản phẩm = "Laptops" AND quantity >= 2 |
| target | PromotionMethod | Cái gì được giảm? | Danh mục sản phẩm = "Accessories" |
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. Promotion Aggregate (Promotion + Method + Rules)
Tạo một promotion hoàn chỉnh cùng method và mọi rule trong một transaction liền mạch duy nhất:
POST /promotions/aggregate
Content-Type: application/json
Authorization: Bearer <token>
{
"promotion": {
"code": "NEWUSER20",
"name": { "en": "New User 20% Off" },
"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 transaction: Mọi entity được tạo hoặc không cái nào (rollback khi có lỗi).
4. Áp dụng Khuyến mãi
⚠️ Chưa hoạt động.
PromotionComputeService(promotion-compute.service.ts) hiện đang tắt (đổi tên thành.ts-disable). Các ví dụ dưới đây ghi lại hợp đồng API dự định; việc tính giảm giá thực tế chưa active.
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 - 10% off"
}
]
}4.2. Khuyến mãi Tự động
Khuyến mãi tự động (isAutomatic: true) được đánh giá trong checkout mà không cần mã khuyến mãi:
// During checkout flow
const promotions = await promotionRepository.findAutomaticPromotions({
effectiveDate: new Date()
});
for (const promotion of promotions) {
const result = await promotionComputeService.applyPromotion({
promotion,
context: orderContext
});
if (result.isApplicable) {
// Apply discount to order
}
}5. Ví dụ Khuyến mãi BuyGet
Kịch bản: "Mua 2 laptop, tặng 1 chuột miễn phí"
{
"promotion": {
"code": "LAPTOP2GET1",
"type": "BUY_GET",
"name": { "en": "Buy 2 Laptops Get 1 Mouse Free" }
},
"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á source rule: Tìm item có
categoryId = "laptops"ANDsum(quantity) >= 2 - Nếu source pass, đánh giá target rule: Tìm item có
categoryId = "accessories"ANDsubcategoryId = "mouse" - Áp giảm 100% cho chuột giá thấp nhất (tối đa
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>Đổi trạng thái từ DRAFT → ACTIVATED. Kiểm tra promotion có method hợp lệ gắn kèm.
6.2. Vô hiệu hoá Khuyến mãi
PUT /promotions/:id/deactivate
Authorization: Bearer <token>Đổi trạng thái từ ACTIVATED → DEACTIVATED. Promotion có thể kích hoạt lại sau.
6.3. Xoá Khuyến mãi (Cascade)
DELETE /promotions/:id
Authorization: Bearer <token>Soft-delete:
- Entity Promotion
- PromotionMethod liên kết
- Mọi Rule gắn kèm (eligibility, source, target)
Mọi thao tác xoá đều transactional (rollback khi có lỗi).
7. Thao tác Repository
7.1. Promotion Repository
| Phương thức | Mô tả |
|---|---|
findByCode() | Tìm promotion active theo mã (loại trừ đã xoá, lọc theo thời gian) |
findAutomaticPromotions() | Lấy mọi promotion auto-apply active tại ngày cho trước |
findActivePromotions() | Lấy mọi promotion active trong phạm vi ngày |
isUsageLimitReached() | Kiểm tra promotion đã đạt giới hạn dùng chưa |
incrementUsageCount() | Tăng usageCount một cách liền mạch (raw SQL) |
updateRulesCount() | Cập nhật rulesCount denormalize |
findByStatus() | Lọc promotion theo trạng thái |
7.2. PromotionMethod Repository
| Phương thức | Mô tả |
|---|---|
findByPromotionId() | Lấy method cho promotion (quan hệ 1:1) |
updateSourceRulesCount() | Cập nhật sourceRulesCount denormalize |
updateTargetRulesCount() | Cập nhật targetRulesCount denormalize |
7.3. Rule Repository (Riêng Promotion)
| Phương thức | Mô tả |
|---|---|
findEligibilityRulesByPromotionId() | Lấy rule eligibility (metadata.context = 'eligibility') |
findSourceRulesByMethodId() | Lấy rule source BuyGet (metadata.context = 'source') |
findTargetRulesByMethodId() | Lấy rule target (metadata.context = 'target') |
deleteByPromotionId() | Cascade xoá mọi rule eligibility của promotion |
deleteByMethodId() | Cascade xoá mọi rule method (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(), discount được hardcode vì compute service đã tắt:
// Hardcoded - PromotionComputeService is disabled
discount: '0', // TODO: Apply promotions here8.2. Tích hợp Dự kiến
// Future implementation
const applicablePromotions = await this.promotionComputeService.applyAutomaticPromotions({
context: pricingContext
});
const discount = applicablePromotions.reduce((sum, promo) =>
sum + parseFloat(promo.discountAmount), 0
);9. Quyết định Thiết kế Chính
| Quyết định | Lý do |
|---|---|
| Hệ thống Rule đa hình | Một bảng Rule phục vụ Fare, Tax, và Promotion - giảm độ phức tạp schema |
| Metadata context cho Rule | Dùng quy ước metadata.context thay vì trường riêng - linh hoạt và mở rộng được |
| 1:1 Promotion:Method | Đơn giản hoá model so với mảng ApplicationMethod của Medusa.js - đủ cho nhu cầu hiện tại |
| Đếm denormalize | rulesCount, sourceRulesCount, targetRulesCount - tránh join cho truy vấn thường gặp |
| Hỗ trợ i18n | Mọi text hướng người dùng (name, description) lưu 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 liền mạch | Một transaction cho Promotion + Method + Rules - nhất quán dữ liệu |
| Compute service tách riêng | Logic tính toán cách ly khỏi CRUD - tách bạch 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 phương thức riêng promotion |
| Controllers | ✅ CRUD cơ bản | Dùng pattern 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 - cố ý loại khỏi build |
| Pricing Integration | ❌ TODO | Chưa tích hợp vào luồng định giá động |
| Request Models | ✅ Hoàn thành | Create, update, aggregate, apply request |
| Response Models | ✅ Hoàn thành | Aggregate và application response |
11. Tài liệu Liên quan
- Pricing Service - Tổng quan package và kiến trúc
- Fare System - Pattern đánh giá rule (chia sẻ với promotion)
- Tax System - Luồng tính thuế
- Sale Service - Quản lý order và tích hợp checkout
- Core Database Schema - Định nghĩa bảng Promotion