Skip to content

Promotion System

1. Overview

PropertyValue
StatusBeta — CRUD only; discount calculation not yet wired into either pipeline
Ownerpricing-team
ServicesPromotionService (CRUD); promotion-compute.service.ts-disable (calculation, disabled)
Controllers / RoutesPromotionController /promotions (POST /aggregate, PATCH /{id}/aggregate), PromotionMethodController /promotion-methods
Depends onPromotion, PromotionMethod, Rule schemas (@nx/core)

Manages promotional campaigns through Promotions (campaign + eligibility rules), PromotionMethods (discount method with source/target rules), and Rules (context conditions). Supports manual codes, automatic promotions, percentage/fixed discounts, BuyGet campaigns, and multi-allocation strategies.

⚠️ Discount calculation is not implemented. The compute service is disabled; neither v1 nor v2 applies promotions, so calculate endpoints return discount: '0'. CRUD works fully. Discounts must be applied externally until the calculator lands.

The aggregate endpoints create/update a promotion with its method and rules atomically in one transaction (one round-trip). Service catalogue and identity card: see Pricing Service index. REST reference: live at /v1/api/pricing/doc/openapi.json.

2. Data Model

2.1. Promotion Types

TypeDescriptionUse Case
STANDARDSimple percentage or fixed discount"10% off entire order", "$5 off shipping"
BUYGETConditional buy X get Y"Buy 2 get 1 free", "Buy $100 get 20% off accessories"

2.2. Promotion Status Lifecycle

StatusDescription
DRAFTCreated but not yet active — can be edited freely
ACTIVATEDLive promotion — can be applied to orders
DEACTIVATEDTemporarily paused — can be reactivated
EXPIREDPast effectiveTo date — read-only
ARCHIVEDPermanently disabled — historical record only

2.3. Target Types

TypeApplies ToExample
ITEMSIndividual order items matching target rules"10% off shoes category"
ORDEREntire order subtotal"15% off orders over $100"
SHIPPINGShipping cost"Free shipping for orders over $50"

2.4. Allocation Strategies

StrategyBehaviorExample
EACHDiscount applied to each matching item individually"10% off each item in Electronics" → 3 items = 3× discount
ACROSSDiscount distributed across all matching items"$10 off Electronics" → $10 split among all matching items
ONCEDiscount applied once regardless of quantity"Buy 2 get 1 free" → Applied once per qualifying set

2.5. Rule Context Convention

Rules are distinguished by metadata.context:

ContextAttached ToPurposeExample
eligibilityPromotionWho can use this promotion?Customer group = "VIP"
sourcePromotionMethodWhat must be bought? (BuyGet only)Product category = "Laptops" AND quantity >= 2
targetPromotionMethodWhat gets discounted?Product category = "Accessories"

3. Promotion Creation Flow

3.1. Simple Promotion (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. Promotion Aggregate (Promotion + Method + Rules)

Create a complete promotion with method and all rules in a single atomic transaction:

http
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" }
    }
  ]
}

Transaction guarantee: All entities created or none (rollback on any error).

4. Promotion Application

⚠️ Not yet functional. The PromotionComputeService (promotion-compute.service.ts) is currently disabled (renamed to .ts-disable). The examples below document the intended API contract; actual discount computation is not active.

4.1. Manual Application

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"
      }
    ]
  }
}

Response:

json
{
  "isApplicable": true,
  "discountAmount": "100000",
  "itemDiscounts": [
    {
      "itemId": "item-1",
      "discountAmount": "100000",
      "reason": "SUMMER2026 - 10% off"
    }
  ]
}

4.2. Automatic Promotions

Automatic promotions (isAutomatic: true) are evaluated during checkout without requiring a promo code:

typescript
// 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. BuyGet Promotion Example

Scenario: "Buy 2 laptops, get 1 mouse free"

json
{
  "promotion": {
    "code": "LAPTOP2GET1",
    "type": "BUYGET",
    "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:

  1. Evaluate source rules: Find items where categoryId = "laptops" AND sum(quantity) >= 2
  2. If source passes, evaluate target rules: Find items where categoryId = "accessories" AND subcategoryId = "mouse"
  3. Apply 100% discount to lowest-priced mouse (up to buyGetTargetQuantity = 1)

6. Promotion Management Operations

6.1. Activate Promotion

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

Changes status from DRAFTACTIVATED. Validates that promotion has a valid method attached.

6.2. Deactivate Promotion

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

Changes status from ACTIVATEDDEACTIVATED. Promotion can be reactivated later.

6.3. Delete Promotion (Cascade)

http
DELETE /promotions/:id
Authorization: Bearer <token>

Soft-deletes:

  • Promotion entity
  • Associated PromotionMethod
  • All attached Rules (eligibility, source, target)

All deletions are transactional (rollback on error).

7. Repository Operations

7.1. Promotion Repository

MethodDescription
findByCode()Find active promotion by code (excludes deleted, filters by time)
findAutomaticPromotions()Get all auto-apply promotions active at given date
findActivePromotions()Get all active promotions within date range
isUsageLimitReached()Check if promotion has reached usage limit
incrementUsageCount()Atomic increment of usageCount (raw SQL)
updateRulesCount()Update denormalized rulesCount
findByStatus()Filter promotions by status

7.2. PromotionMethod Repository

MethodDescription
findByPromotionId()Get method for promotion (1:1 relationship)
updateSourceRulesCount()Update denormalized sourceRulesCount
updateTargetRulesCount()Update denormalized targetRulesCount

7.3. Rule Repository (Promotion-Specific)

MethodDescription
findEligibilityRulesByPromotionId()Get eligibility rules (metadata.context = 'eligibility')
findSourceRulesByMethodId()Get BuyGet source rules (metadata.context = 'source')
findTargetRulesByMethodId()Get target rules (metadata.context = 'target')
deleteByPromotionId()Cascade delete all promotion eligibility rules
deleteByMethodId()Cascade delete all method rules (source + target)

8. Integration with Pricing Flow

8.1. Current State (Disabled)

In PricingService.calculate(), discount is hardcoded because the compute service is disabled:

typescript
// Hardcoded — PromotionComputeService is disabled
discount: '0', // TODO: Apply promotions here

8.2. Planned Integration

typescript
// Future implementation
const applicablePromotions = await this.promotionComputeService.applyAutomaticPromotions({
  context: pricingContext
});

const discount = applicablePromotions.reduce((sum, promo) => 
  sum + parseFloat(promo.discountAmount), 0
);

9. Key Design Decisions

DecisionRationale
Polymorphic Rule systemSingle Rule table serves Fare, Tax, and Promotion — reduces schema complexity
Rule context metadataUses metadata.context convention instead of separate fields — flexible and extensible
1:1 Promotion:MethodSimplifies model vs Medusa.js's ApplicationMethod array — sufficient for current needs
Denormalized countsrulesCount, sourceRulesCount, targetRulesCount — avoids joins for common queries
i18n supportAll user-facing text (name, description) stored as JSONB — multi-language ready
Status lifecycle5 states (DRAFT → ACTIVATED → DEACTIVATED → EXPIRED → ARCHIVED) — clear state machine
Atomic aggregate creationSingle transaction for Promotion + Method + Rules — data consistency
Separate compute serviceCalculation logic isolated from CRUD — separation of concerns

10. Implementation Status

ComponentStatusNotes
Schemas✅ CompletePromotion, PromotionMethod, Rule (polymorphic)
Repositories✅ CompleteExtended with promotion-specific methods
Controllers✅ Basic CRUDUsing ControllerFactory pattern
Management Service✅ CompletePromotionService with 6 methods (CRUD, activate, deactivate, delete)
Compute Service❌ Disabledpromotion-compute.service.ts renamed to .ts-disable — intentionally excluded from build
Pricing Integration❌ TODONot yet integrated into dynamic pricing flow
Request Models✅ CompleteCreate, update, aggregate, apply requests
Response Models✅ CompleteAggregate and application responses

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