Skip to content

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

MethodPathMô tả
POST/promotionsTạo khuyến mãi
GET/promotionsDanh sách khuyến mãi
GET/promotions/:idLấy khuyến mãi theo ID
PUT/promotions/:idCập nhật khuyến mãi
DELETE/promotions/:idXóa khuyến mãi
POST/promotions/aggregateTạo khuyến mãi với method và rules trong một request
PATCH/promotions/:id/aggregateCập nhật khuyến mãi, method và rules trong một request
POST/promotion-methodsTạo promotion method
GET/promotion-methodsDanh sách promotion methods
GET/promotion-methods/:idLấy promotion method theo ID
PUT/promotion-methods/:idCập nhật promotion method
DELETE/promotion-methods/:idXó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ạiMô tảTrường hợp sử dụng
STANDARDGiả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"
BUYGETMua 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áiMô tả
DRAFTĐã tạo nhưng chưa hoạt động — có thể chỉnh sửa tự do
ACTIVATEDKhuyến mãi đang hoạt động — có thể áp dụng cho đơn hàng
DEACTIVATEDTạm dừng — có thể kích hoạt lại
EXPIREDQua ngày effectiveTo — chỉ đọc
ARCHIVEDVô 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 choVí dụ
ITEMSCác mục đơn hàng riêng lẻ khớp quy tắc đích"Giảm 10% danh mục giày"
ORDERToàn bộ tổng phụ đơn hàng"Giảm 15% đơn hàng trên 1.000.000đ"
SHIPPINGChi 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ượcHành viVí dụ
EACHGiả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á
ACROSSGiả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
ONCEGiả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ảnhGắn vàoMục đíchVí dụ
eligibilityPromotionAi có thể sử dụng khuyến mãi này?Nhóm khách hàng = "VIP"
sourcePromotionMethodPhải mua gì? (Chỉ BuyGet)Danh mục sản phẩm = "Laptop" VÀ số lượng >= 2
targetPromotionMethodGiả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)

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

http
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

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

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

typescript
// 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"

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

  1. Đánh giá quy tắc nguồn: Tìm các mục có categoryId = "laptops"sum(quantity) >= 2
  2. Nếu nguồn đạt, đánh giá quy tắc đích: Tìm các mục có categoryId = "accessories"subcategoryId = "mouse"
  3. Á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

http
PUT /promotions/:id/activate
Authorization: Bearer <token>

Thay đổi trạng thái từ DRAFTACTIVATED. 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

http
PUT /promotions/:id/deactivate
Authorization: Bearer <token>

Thay đổi trạng thái từ ACTIVATEDDEACTIVATED. Khuyến mãi có thể được kích hoạt lại sau.

6.3. Xóa khuyến mãi (Cascade)

http
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ứcMô 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ứcMô 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ứcMô 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:

typescript
// Hardcode — PromotionComputeService đã bị tắt
discount: '0', // TODO: Áp dụng khuyến mãi tại đây

8.2. Triển khai dự kiến

typescript
// 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 địnhLý do
Hệ thống quy tắc đa hìnhBảng Rule đơn phục vụ Fare, Tax và Promotion — giảm độ phức tạp schema
Metadata ngữ cảnh quy tắcSử 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 denormalizedrulesCount, sourceRulesCount, targetRulesCount — tránh joins cho các truy vấn phổ biến
Hỗ trợ i18nTấ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ái5 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êngLogic 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ầnTrạng tháiGhi chú
Schemas✅ Hoàn thànhPromotion, PromotionMethod, Rule (đa hình)
Repositories✅ Hoàn thànhMở rộng với các phương thức đặc biệt cho khuyến mãi
Controllers✅ CRUD cơ bảnSử dụng mẫu ControllerFactory
Management Service✅ Hoàn thànhPromotionService với 6 phương thức (CRUD, activate, deactivate, delete)
Compute Service❌ Đã tắtpromotion-compute.service.ts đổi tên thành .ts-disable — bị loại khỏi build có chủ ý
Tích hợp định giá❌ TODOChưa tích hợp vào luồng định giá động
Request Models✅ Hoàn thànhCreate, update, aggregate, apply requests
Response Models✅ Hoàn thànhAggregate và application responses

11. Tài liệu liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.