diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index ee44ad5445..7ba8ffcd66 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -1,6 +1,6 @@ import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" -import { PromotionType } from "@medusajs/utils" +import { ApplicationMethodType, PromotionType } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" @@ -49,7 +49,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -60,7 +60,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -95,7 +95,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, adjustments: [ { id: "test-adjustment", @@ -106,7 +106,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, }, ], }) @@ -138,12 +138,12 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, }, { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, adjustments: [ { id: "test-adjustment", @@ -162,221 +162,37 @@ describe("Promotion Service: computeActions", () => { }) describe("when promotion is for items and allocation is each", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "200", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - is_automatic: true, - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "30", - max_quantity: 2, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -386,7 +202,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -396,8 +212,8 @@ describe("Promotion Service: computeActions", () => { }, { id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + quantity: 5, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -406,97 +222,410 @@ describe("Promotion Service: computeActions", () => { }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 20, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 500, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -506,7 +635,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -516,8 +645,8 @@ describe("Promotion Service: computeActions", () => { }, { id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + quantity: 5, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -526,359 +655,406 @@ describe("Promotion Service: computeActions", () => { }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + customer: { + customer_group: { + id: "VIP", + }, }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 3, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", }, - ], + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 45, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 2, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 10.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 10000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) describe("when promotion is for items and allocation is across", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "30", - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -887,8 +1063,8 @@ describe("Promotion Service: computeActions", () => { items: [ { id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, + quantity: 2, + subtotal: 200, product_category: { id: "catg_cotton", }, @@ -898,8 +1074,8 @@ describe("Promotion Service: computeActions", () => { }, { id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + quantity: 2, + subtotal: 600, product_category: { id: "catg_cotton", }, @@ -908,95 +1084,54 @@ describe("Promotion Service: computeActions", () => { }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 7.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 22.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -1005,8 +1140,8 @@ describe("Promotion Service: computeActions", () => { items: [ { id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, + quantity: 2, + subtotal: 200, product_category: { id: "catg_cotton", }, @@ -1016,8 +1151,8 @@ describe("Promotion Service: computeActions", () => { }, { id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + quantity: 2, + subtotal: 600, product_category: { id: "catg_cotton", }, @@ -1026,344 +1161,911 @@ describe("Promotion Service: computeActions", () => { }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "1500", - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "30", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + customer: { + customer_group: { + id: "VIP", + }, }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 7.5, + code: "PROMOTION_TEST", }, - ], + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 22.5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 12.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 37.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) describe("when promotion is for shipping_method and allocation is each", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", - }, - }, - { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", - }, - }, - { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions( - [], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1372,93 +2074,75 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 250, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 150, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - }, - { prevent_auto_promotions: true } - ) + }) - expect(result).toEqual([]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -1467,111 +2151,365 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 250, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 150, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + expect(result).toEqual([]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 250, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1580,369 +2518,685 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 150, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 250, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", - }, - }, - ], + expect(result).toEqual([]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) describe("when promotion is for shipping_method and allocation is across", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", - }, - }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", - }, - }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, - ], - }) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) - - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) - - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1951,115 +3205,74 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 83.33333333333331, - code: "PROMOTION_TEST_2", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 16.66666666666667, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1000", - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -2068,154 +3281,862 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 500, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 100, - code: "PROMOTION_TEST", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", + }, + ]) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotion2] = await service.create([ { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", - }, - }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 83.33333333333331, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 16.66666666666667, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 45, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 9, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) @@ -2251,7 +4172,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2262,7 +4183,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2321,7 +4242,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2332,7 +4253,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2412,7 +4333,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2423,7 +4344,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2516,7 +4437,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2527,7 +4448,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2552,18 +4473,6 @@ describe("Promotion Service: computeActions", () => { amount: 150, code: "PROMOTION_TEST", }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, ]) }) }) @@ -2615,7 +4524,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2632,7 +4541,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -2708,7 +4617,7 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, @@ -2721,14 +4630,14 @@ describe("Promotion Service: computeActions", () => { }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, @@ -2770,7 +4679,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2781,7 +4690,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2792,7 +4701,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2862,7 +4771,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2873,7 +4782,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2884,7 +4793,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2947,7 +4856,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2958,7 +4867,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2969,7 +4878,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -3045,7 +4954,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -3056,7 +4965,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -3067,7 +4976,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 909f31dbc4..dc36e06613 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -1,16 +1,17 @@ +import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { + ApplicationMethodTargetType, ApplicationMethodType, CampaignBudgetType, PromotionType, } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" import { createPromotions } from "../../../__fixtures__/promotion" import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" jest.setTimeout(30000) @@ -114,6 +115,24 @@ describe("Promotion Service", () => { ) }) + it("should throw error when percentage type and value is greater than 100", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: ApplicationMethodTargetType.ORDER, + value: "1000", + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "Application Method value should be a percentage number between 0 and 100" + ) + }) + it("should throw an error when both campaign and campaign_id are provided", async () => { const startsAt = new Date("01/01/2023") const endsAt = new Date("01/01/2023") @@ -655,7 +674,7 @@ describe("Promotion Service", () => { is_automatic: true, code: "TEST", type: PromotionType.BUYGET, - }, + } as any, ]) expect(updatedPromotion).toEqual( @@ -899,12 +918,12 @@ describe("Promotion Service", () => { value: "200", target_type: "items", }, - }, + } as any, { id: "promotion-id-2", code: "PROMOTION_2", type: PromotionType.STANDARD, - }, + } as any, ]) }) diff --git a/packages/promotion/src/utils/compute-actions/buy-get.ts b/packages/promotion/src/utils/compute-actions/buy-get.ts index cc6eaa44e1..d3bcc3f68d 100644 --- a/packages/promotion/src/utils/compute-actions/buy-get.ts +++ b/packages/promotion/src/utils/compute-actions/buy-get.ts @@ -46,7 +46,10 @@ export function getComputedActionsForBuyGet( const validItemsForTargetRules = itemsContext .filter((item) => areRulesValidForContext(targetRules, item)) .sort((a, b) => { - return b.unit_price - a.unit_price + const aPrice = a.subtotal / a.quantity + const bPrice = b.subtotal / b.quantity + + return bPrice - aPrice }) let remainingQtyToApply = applyToQuantity @@ -54,7 +57,7 @@ export function getComputedActionsForBuyGet( for (const method of validItemsForTargetRules) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 const multiplier = Math.min(method.quantity, remainingQtyToApply) - const amount = method.unit_price * multiplier + const amount = (method.subtotal / method.quantity) * multiplier const newRemainingQtyToApply = remainingQtyToApply - multiplier if (newRemainingQtyToApply < 0 || amount <= 0) { diff --git a/packages/promotion/src/utils/compute-actions/items.ts b/packages/promotion/src/utils/compute-actions/items.ts index afbf400b56..555a27548f 100644 --- a/packages/promotion/src/utils/compute-actions/items.ts +++ b/packages/promotion/src/utils/compute-actions/items.ts @@ -5,6 +5,7 @@ import { import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -67,10 +68,15 @@ export function applyPromotionToItems( method.quantity, applicationMethod?.max_quantity! ) - const promotionValue = - parseFloat(applicationMethod!.value!) * quantityMultiplier - const applicableTotal = - method.unit_price * quantityMultiplier - appliedPromoValue + const totalItemValue = + (method.subtotal / method.quantity) * quantityMultiplier + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = totalItemValue - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -106,18 +112,32 @@ export function applyPromotionToItems( ) { const totalApplicableValue = items!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price * method.quantity - appliedPromoValue + return ( + acc + + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + ) }, 0) for (const method of items!) { - const promotionValue = parseFloat(applicationMethod!.value!) const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = - method.unit_price * method.quantity - appliedPromoValue + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + + if (applicableTotal <= 0) { + continue + } // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { @@ -135,6 +155,8 @@ export function applyPromotionToItems( continue } + methodIdPromoValueMap.set(method.id, appliedPromoValue + amount) + computedActions.push({ action: ComputedActions.ADD_ITEM_ADJUSTMENT, item_id: method.id, diff --git a/packages/promotion/src/utils/compute-actions/shipping-methods.ts b/packages/promotion/src/utils/compute-actions/shipping-methods.ts index 440021f482..6e96db1fd2 100644 --- a/packages/promotion/src/utils/compute-actions/shipping-methods.ts +++ b/packages/promotion/src/utils/compute-actions/shipping-methods.ts @@ -2,6 +2,7 @@ import { PromotionTypes } from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -55,8 +56,13 @@ export function applyPromotionToShippingMethods( if (allocation === ApplicationMethodAllocation.EACH) { for (const method of shippingMethods!) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price - appliedPromoValue + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = method.subtotal - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -89,7 +95,7 @@ export function applyPromotionToShippingMethods( const totalApplicableValue = shippingMethods!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price - appliedPromoValue + return acc + method.subtotal - appliedPromoValue }, 0) if (totalApplicableValue <= 0) { @@ -98,14 +104,19 @@ export function applyPromotionToShippingMethods( for (const method of shippingMethods!) { const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price + const applicableTotal = method.subtotal const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue - appliedPromoValue + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = + (promotionValue / 100) * (applicableTotal - appliedPromoValue) + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { diff --git a/packages/promotion/src/utils/validations/application-method.ts b/packages/promotion/src/utils/validations/application-method.ts index 99c6296349..7d022f0e9d 100644 --- a/packages/promotion/src/utils/validations/application-method.ts +++ b/packages/promotion/src/utils/validations/application-method.ts @@ -37,11 +37,23 @@ export function validateApplicationMethodAttributes( const applyToQuantity = data.apply_to_quantity || applicationMethod?.apply_to_quantity const targetType = data.target_type || applicationMethod?.target_type + const type = data.type || applicationMethod?.type const applicationMethodType = data.type || applicationMethod?.type + const value = parseFloat(data.value! || applicationMethod?.value!) const maxQuantity = data.max_quantity || applicationMethod.max_quantity const allocation = data.allocation || applicationMethod.allocation const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType) + if ( + type === ApplicationMethodType.PERCENTAGE && + (value <= 0 || value > 100) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Application Method value should be a percentage number between 0 and 100` + ) + } + if (promotion?.type === PromotionType.BUYGET) { if (!isPresent(applyToQuantity)) { throw new MedusaError( diff --git a/packages/types/src/promotion/common/compute-actions.ts b/packages/types/src/promotion/common/compute-actions.ts index 267da4b5c3..1384daa93f 100644 --- a/packages/types/src/promotion/common/compute-actions.ts +++ b/packages/types/src/promotion/common/compute-actions.ts @@ -51,13 +51,13 @@ export interface ComputeActionAdjustmentLine extends Record { export interface ComputeActionItemLine extends Record { id: string quantity: number - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] } export interface ComputeActionShippingLine extends Record { id: string - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] }