feat(utils,types): add registerUsages for promotions + computeActions consider usage (#6094)
RESOLVES CORE-1639 RESOLVES CORE-1640 RESOLVES CORE-1634
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(utils,types): add registerUsages for promotion's computed actions
|
||||||
+476
-20
@@ -2,6 +2,7 @@ import { IPromotionModuleService } from "@medusajs/types"
|
|||||||
import { PromotionType } from "@medusajs/utils"
|
import { PromotionType } from "@medusajs/utils"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { initialize } from "../../../../src"
|
import { initialize } from "../../../../src"
|
||||||
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
import { DB_URL, MikroOrmWrapper } from "../../../utils"
|
import { DB_URL, MikroOrmWrapper } from "../../../utils"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
@@ -459,10 +460,10 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe("when promotion is for items and allocation is across", () => {
|
it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => {
|
||||||
it("should compute the correct item amendments", async () => {
|
await createCampaigns(repositoryManager)
|
||||||
|
|
||||||
const [createdPromotion] = await service.create([
|
const [createdPromotion] = await service.create([
|
||||||
{
|
{
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
@@ -474,12 +475,13 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
values: ["VIP", "top100"],
|
values: ["VIP", "top100"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
campaign_id: "campaign-id-1",
|
||||||
application_method: {
|
application_method: {
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "each",
|
||||||
value: "200",
|
value: "500",
|
||||||
max_quantity: 2,
|
max_quantity: 5,
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_category.id",
|
attribute: "product_category.id",
|
||||||
@@ -500,7 +502,126 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: "item_cotton_tshirt",
|
id: "item_cotton_tshirt",
|
||||||
quantity: 1,
|
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: [
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
unit_price: 1000,
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
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,
|
unit_price: 100,
|
||||||
product_category: {
|
product_category: {
|
||||||
id: "catg_cotton",
|
id: "catg_cotton",
|
||||||
@@ -512,7 +633,7 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
{
|
{
|
||||||
id: "item_cotton_sweater",
|
id: "item_cotton_sweater",
|
||||||
quantity: 2,
|
quantity: 2,
|
||||||
unit_price: 150,
|
unit_price: 300,
|
||||||
product_category: {
|
product_category: {
|
||||||
id: "catg_cotton",
|
id: "catg_cotton",
|
||||||
},
|
},
|
||||||
@@ -527,13 +648,13 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_tshirt",
|
item_id: "item_cotton_tshirt",
|
||||||
amount: 50,
|
amount: 100,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
item_id: "item_cotton_sweater",
|
item_id: "item_cotton_sweater",
|
||||||
amount: 150,
|
amount: 300,
|
||||||
code: "PROMOTION_TEST",
|
code: "PROMOTION_TEST",
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
@@ -556,7 +677,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "30",
|
value: "30",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_category.id",
|
attribute: "product_category.id",
|
||||||
@@ -584,7 +704,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "50",
|
value: "50",
|
||||||
max_quantity: 1,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_category.id",
|
attribute: "product_category.id",
|
||||||
@@ -676,7 +795,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "500",
|
value: "500",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_category.id",
|
attribute: "product_category.id",
|
||||||
@@ -704,7 +822,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "items",
|
target_type: "items",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "50",
|
value: "50",
|
||||||
max_quantity: 1,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "product_category.id",
|
attribute: "product_category.id",
|
||||||
@@ -778,6 +895,125 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
unit_price: 1000,
|
||||||
|
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", () => {
|
describe("when promotion is for shipping_method and allocation is each", () => {
|
||||||
@@ -1076,6 +1312,119 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
shipping_methods: [
|
||||||
|
{
|
||||||
|
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: "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",
|
||||||
|
unit_price: 1200,
|
||||||
|
shipping_option: {
|
||||||
|
id: "express",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ action: "campaignBudgetExceeded", code: "PROMOTION_TEST" },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("when promotion is for shipping_method and allocation is across", () => {
|
describe("when promotion is for shipping_method and allocation is across", () => {
|
||||||
@@ -1096,7 +1445,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1172,7 +1520,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1200,7 +1547,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1291,7 +1637,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "1000",
|
value: "1000",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1319,7 +1664,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1380,6 +1724,117 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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: [
|
||||||
|
{
|
||||||
|
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: 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: [
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
unit_price: 1200,
|
||||||
|
shipping_option: {
|
||||||
|
id: "express",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ action: "campaignBudgetExceeded", code: "PROMOTION_TEST" },
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("when promotion is for the entire order", () => {
|
describe("when promotion is for the entire order", () => {
|
||||||
@@ -1740,6 +2195,7 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
{
|
{
|
||||||
action: "removeItemAdjustment",
|
action: "removeItemAdjustment",
|
||||||
adjustment_id: "test-adjustment",
|
adjustment_id: "test-adjustment",
|
||||||
|
code: "ADJUSTMENT_CODE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addItemAdjustment",
|
action: "addItemAdjustment",
|
||||||
@@ -1780,7 +2236,6 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
target_type: "shipping_methods",
|
target_type: "shipping_methods",
|
||||||
allocation: "across",
|
allocation: "across",
|
||||||
value: "200",
|
value: "200",
|
||||||
max_quantity: 2,
|
|
||||||
target_rules: [
|
target_rules: [
|
||||||
{
|
{
|
||||||
attribute: "shipping_option.id",
|
attribute: "shipping_option.id",
|
||||||
@@ -1833,6 +2288,7 @@ describe("Promotion Service: computeActions", () => {
|
|||||||
{
|
{
|
||||||
action: "removeShippingMethodAdjustment",
|
action: "removeShippingMethodAdjustment",
|
||||||
adjustment_id: "test-adjustment",
|
adjustment_id: "test-adjustment",
|
||||||
|
code: "ADJUSTMENT_CODE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: "addShippingMethodAdjustment",
|
action: "addShippingMethodAdjustment",
|
||||||
|
|||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import { initialize } from "../../../../src"
|
||||||
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
|
import { DB_URL, MikroOrmWrapper } from "../../../utils"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
describe("Promotion Service: campaign usage", () => {
|
||||||
|
let service: IPromotionModuleService
|
||||||
|
let repositoryManager: SqlEntityManager
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await MikroOrmWrapper.setupDatabase()
|
||||||
|
repositoryManager = MikroOrmWrapper.forkManager()
|
||||||
|
|
||||||
|
await createCampaigns(repositoryManager)
|
||||||
|
|
||||||
|
service = await initialize({
|
||||||
|
database: {
|
||||||
|
clientUrl: DB_URL,
|
||||||
|
schema: process.env.MEDUSA_PROMOTION_DB_SCHEMA,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await MikroOrmWrapper.clearDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("registerUsage", () => {
|
||||||
|
it("should register usage for type spend", async () => {
|
||||||
|
const createdPromotion = await service.create({
|
||||||
|
code: "TEST_PROMO_SPEND",
|
||||||
|
type: "standard",
|
||||||
|
campaign_id: "campaign-id-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_express",
|
||||||
|
amount: 200,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_standard",
|
||||||
|
amount: 500,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign("campaign-id-1", {
|
||||||
|
relations: ["budget"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(campaign.budget).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "spend",
|
||||||
|
limit: 1000,
|
||||||
|
used: 700,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should register usage for type usage", async () => {
|
||||||
|
const createdPromotion = await service.create({
|
||||||
|
code: "TEST_PROMO_USAGE",
|
||||||
|
type: "standard",
|
||||||
|
campaign_id: "campaign-id-2",
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_express",
|
||||||
|
amount: 200,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_standard",
|
||||||
|
amount: 500,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign("campaign-id-2", {
|
||||||
|
relations: ["budget"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(campaign.budget).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "usage",
|
||||||
|
limit: 1000,
|
||||||
|
used: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not throw an error when compute action with code does not exist", async () => {
|
||||||
|
const response = await service
|
||||||
|
.registerUsage([
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_express",
|
||||||
|
amount: 200,
|
||||||
|
code: "DOESNOTEXIST",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(response).toEqual(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not register usage when limit is exceed for type usage", async () => {
|
||||||
|
const createdPromotion = await service.create({
|
||||||
|
code: "TEST_PROMO_USAGE",
|
||||||
|
type: "standard",
|
||||||
|
campaign_id: "campaign-id-2",
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.updateCampaigns({
|
||||||
|
id: "campaign-id-2",
|
||||||
|
budget: { used: 1000, limit: 1000 },
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_express",
|
||||||
|
amount: 200,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_standard",
|
||||||
|
amount: 500,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign("campaign-id-2", {
|
||||||
|
relations: ["budget"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(campaign).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
budget: expect.objectContaining({
|
||||||
|
limit: 1000,
|
||||||
|
used: 1000,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not register usage above limit when exceeded for type spend", async () => {
|
||||||
|
const createdPromotion = await service.create({
|
||||||
|
code: "TEST_PROMO_SPEND",
|
||||||
|
type: "standard",
|
||||||
|
campaign_id: "campaign-id-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.updateCampaigns({
|
||||||
|
id: "campaign-id-1",
|
||||||
|
budget: { used: 900, limit: 1000 },
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.registerUsage([
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_express",
|
||||||
|
amount: 100,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addShippingMethodAdjustment",
|
||||||
|
shipping_method_id: "shipping_method_standard",
|
||||||
|
amount: 100,
|
||||||
|
code: createdPromotion.code!,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const campaign = await service.retrieveCampaign("campaign-id-1", {
|
||||||
|
relations: ["budget"],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(campaign).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
budget: expect.objectContaining({
|
||||||
|
limit: 1000,
|
||||||
|
used: 1000,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -73,12 +73,14 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
|
|||||||
const campaignPromotionIdsMap = new Map<string, string[]>()
|
const campaignPromotionIdsMap = new Map<string, string[]>()
|
||||||
|
|
||||||
data.forEach((campaignData) => {
|
data.forEach((campaignData) => {
|
||||||
const campaignPromotionIds =
|
const campaignPromotionIds = campaignData.promotions?.map((p) => p.id)
|
||||||
campaignData.promotions?.map((p) => p.id) || []
|
|
||||||
|
|
||||||
campaignIds.push(campaignData.id)
|
campaignIds.push(campaignData.id)
|
||||||
promotionIdsToUpsert.push(...campaignPromotionIds)
|
|
||||||
campaignPromotionIdsMap.set(campaignData.id, campaignPromotionIds)
|
if (campaignPromotionIds) {
|
||||||
|
promotionIdsToUpsert.push(...campaignPromotionIds)
|
||||||
|
campaignPromotionIdsMap.set(campaignData.id, campaignPromotionIds)
|
||||||
|
}
|
||||||
|
|
||||||
delete campaignData.promotions
|
delete campaignData.promotions
|
||||||
})
|
})
|
||||||
@@ -109,8 +111,12 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
|
|||||||
const updatedCampaigns = await super.update(data, context)
|
const updatedCampaigns = await super.update(data, context)
|
||||||
|
|
||||||
for (const updatedCampaign of updatedCampaigns) {
|
for (const updatedCampaign of updatedCampaigns) {
|
||||||
const upsertPromotionIds =
|
const upsertPromotionIds = campaignPromotionIdsMap.get(updatedCampaign.id)
|
||||||
campaignPromotionIdsMap.get(updatedCampaign.id) || []
|
|
||||||
|
if (!upsertPromotionIds) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const existingPromotionIds = (
|
const existingPromotionIds = (
|
||||||
existingCampaignsMap.get(updatedCampaign.id)?.promotions || []
|
existingCampaignsMap.get(updatedCampaign.id)?.promotions || []
|
||||||
).map((p) => p.id)
|
).map((p) => p.id)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
|
CampaignBudgetType,
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
@@ -90,6 +91,110 @@ export default class PromotionModuleService<
|
|||||||
return joinerConfig
|
return joinerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InjectManager("baseRepository_")
|
||||||
|
async registerUsage(
|
||||||
|
computedActions: PromotionTypes.UsageComputedActions[],
|
||||||
|
@MedusaContext() sharedContext: Context = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const promotionCodes = computedActions
|
||||||
|
.map((computedAction) => computedAction.code)
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const promotionCodeCampaignBudgetMap = new Map<
|
||||||
|
string,
|
||||||
|
UpdateCampaignBudgetDTO
|
||||||
|
>()
|
||||||
|
const promotionCodeUsageMap = new Map<string, boolean>()
|
||||||
|
|
||||||
|
const existingPromotions = await this.list(
|
||||||
|
{ code: promotionCodes },
|
||||||
|
{ relations: ["application_method", "campaign", "campaign.budget"] },
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
|
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
||||||
|
existingPromotions.map((promotion) => [promotion.code!, promotion])
|
||||||
|
)
|
||||||
|
|
||||||
|
for (let computedAction of computedActions) {
|
||||||
|
if (!ComputeActionUtils.canRegisterUsage(computedAction)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const promotion = existingPromotionsMap.get(computedAction.code)
|
||||||
|
|
||||||
|
if (!promotion) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudget = promotion.campaign?.budget
|
||||||
|
|
||||||
|
if (!campaignBudget) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
||||||
|
const campaignBudgetData = promotionCodeCampaignBudgetMap.get(
|
||||||
|
campaignBudget.id
|
||||||
|
) || { id: campaignBudget.id, used: campaignBudget.used || 0 }
|
||||||
|
|
||||||
|
campaignBudgetData.used =
|
||||||
|
(campaignBudgetData.used || 0) + computedAction.amount
|
||||||
|
|
||||||
|
if (
|
||||||
|
campaignBudget.limit &&
|
||||||
|
campaignBudgetData.used > campaignBudget.limit
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
promotionCodeCampaignBudgetMap.set(
|
||||||
|
campaignBudget.id,
|
||||||
|
campaignBudgetData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (campaignBudget.type === CampaignBudgetType.USAGE) {
|
||||||
|
const promotionAlreadyUsed =
|
||||||
|
promotionCodeUsageMap.get(promotion.code!) || false
|
||||||
|
|
||||||
|
if (promotionAlreadyUsed) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudgetData = {
|
||||||
|
id: campaignBudget.id,
|
||||||
|
used: (campaignBudget.used || 0) + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
campaignBudget.limit &&
|
||||||
|
campaignBudgetData.used > campaignBudget.limit
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
promotionCodeCampaignBudgetMap.set(
|
||||||
|
campaignBudget.id,
|
||||||
|
campaignBudgetData
|
||||||
|
)
|
||||||
|
|
||||||
|
promotionCodeUsageMap.set(promotion.code!, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||||
|
|
||||||
|
for (const [_, campaignBudgetData] of promotionCodeCampaignBudgetMap) {
|
||||||
|
campaignBudgetsData.push(campaignBudgetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.campaignBudgetService_.update(
|
||||||
|
campaignBudgetsData,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async computeActions(
|
async computeActions(
|
||||||
promotionCodesToApply: string[],
|
promotionCodesToApply: string[],
|
||||||
applicationContext: PromotionTypes.ComputeActionContext,
|
applicationContext: PromotionTypes.ComputeActionContext,
|
||||||
@@ -140,6 +245,8 @@ export default class PromotionModuleService<
|
|||||||
"application_method.target_rules.values",
|
"application_method.target_rules.values",
|
||||||
"rules",
|
"rules",
|
||||||
"rules.values",
|
"rules.values",
|
||||||
|
"campaign",
|
||||||
|
"campaign.budget",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -166,6 +273,7 @@ export default class PromotionModuleService<
|
|||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "removeItemAdjustment",
|
action: "removeItemAdjustment",
|
||||||
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
||||||
|
code: appliedCode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +281,7 @@ export default class PromotionModuleService<
|
|||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "removeShippingMethodAdjustment",
|
action: "removeShippingMethodAdjustment",
|
||||||
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
||||||
|
code: appliedCode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,6 +653,7 @@ export default class PromotionModuleService<
|
|||||||
!allowedAllocationForQuantity.includes(applicationMethodData.allocation)
|
!allowedAllocationForQuantity.includes(applicationMethodData.allocation)
|
||||||
) {
|
) {
|
||||||
applicationMethodData.max_quantity = null
|
applicationMethodData.max_quantity = null
|
||||||
|
existingApplicationMethod.max_quantity = null
|
||||||
}
|
}
|
||||||
|
|
||||||
validateApplicationMethodAttributes({
|
validateApplicationMethodAttributes({
|
||||||
@@ -952,12 +1062,8 @@ export default class PromotionModuleService<
|
|||||||
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
|
||||||
|
|
||||||
const existingCampaigns = await this.listCampaigns(
|
const existingCampaigns = await this.listCampaigns(
|
||||||
{
|
{ id: campaignIds },
|
||||||
id: campaignIds,
|
{ relations: ["budget"] },
|
||||||
},
|
|
||||||
{
|
|
||||||
relations: ["budget"],
|
|
||||||
},
|
|
||||||
sharedContext
|
sharedContext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./items"
|
export * from "./items"
|
||||||
export * from "./order"
|
export * from "./order"
|
||||||
export * from "./shipping-methods"
|
export * from "./shipping-methods"
|
||||||
|
export * from "./usage"
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
ApplicationMethodAllocation,
|
ApplicationMethodAllocation,
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
|
ComputedActions,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { areRulesValidForContext } from "../validations"
|
import { areRulesValidForContext } from "../validations"
|
||||||
|
import { computeActionForBudgetExceeded } from "./usage"
|
||||||
|
|
||||||
export function getComputedActionsForItems(
|
export function getComputedActionsForItems(
|
||||||
promotion: PromotionTypes.PromotionDTO,
|
promotion: PromotionTypes.PromotionDTO,
|
||||||
@@ -61,22 +63,35 @@ export function applyPromotionToItems(
|
|||||||
) {
|
) {
|
||||||
for (const method of items!) {
|
for (const method of items!) {
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
const quantityMultiplier = Math.min(
|
||||||
|
method.quantity,
|
||||||
|
applicationMethod?.max_quantity!
|
||||||
|
)
|
||||||
|
const promotionValue =
|
||||||
|
parseFloat(applicationMethod!.value!) * quantityMultiplier
|
||||||
const applicableTotal =
|
const applicableTotal =
|
||||||
method.unit_price *
|
method.unit_price * quantityMultiplier - appliedPromoValue
|
||||||
Math.min(method.quantity, applicationMethod?.max_quantity!) -
|
|
||||||
appliedPromoValue
|
|
||||||
|
|
||||||
const amount = Math.min(promotionValue, applicableTotal)
|
const amount = Math.min(promotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||||
|
promotion,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if (budgetExceededAction) {
|
||||||
|
computedActions.push(budgetExceededAction)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "addItemAdjustment",
|
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||||
item_id: method.id,
|
item_id: method.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
@@ -91,35 +106,37 @@ export function applyPromotionToItems(
|
|||||||
) {
|
) {
|
||||||
const totalApplicableValue = items!.reduce((acc, method) => {
|
const totalApplicableValue = items!.reduce((acc, method) => {
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
return (
|
return acc + method.unit_price * method.quantity - appliedPromoValue
|
||||||
acc +
|
|
||||||
method.unit_price *
|
|
||||||
Math.min(method.quantity, applicationMethod?.max_quantity!) -
|
|
||||||
appliedPromoValue
|
|
||||||
)
|
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
for (const method of items!) {
|
for (const method of items!) {
|
||||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
const promotionValue = parseFloat(applicationMethod!.value!)
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
|
|
||||||
const applicableTotal =
|
const applicableTotal =
|
||||||
method.unit_price *
|
method.unit_price * method.quantity - appliedPromoValue
|
||||||
Math.min(method.quantity, applicationMethod?.max_quantity!) -
|
|
||||||
appliedPromoValue
|
|
||||||
|
|
||||||
// TODO: should we worry about precision here?
|
// TODO: should we worry about precision here?
|
||||||
const applicablePromotionValue =
|
const applicablePromotionValue =
|
||||||
(applicableTotal / totalApplicableValue) * promotionValue
|
(applicableTotal / totalApplicableValue) * promotionValue
|
||||||
|
|
||||||
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||||
|
promotion,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if (budgetExceededAction) {
|
||||||
|
computedActions.push(budgetExceededAction)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "addItemAdjustment",
|
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||||
item_id: method.id,
|
item_id: method.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import { PromotionTypes } from "@medusajs/types"
|
|||||||
import {
|
import {
|
||||||
ApplicationMethodAllocation,
|
ApplicationMethodAllocation,
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
|
ComputedActions,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { areRulesValidForContext } from "../validations"
|
import { areRulesValidForContext } from "../validations"
|
||||||
|
import { computeActionForBudgetExceeded } from "./usage"
|
||||||
|
|
||||||
export function getComputedActionsForShippingMethods(
|
export function getComputedActionsForShippingMethods(
|
||||||
promotion: PromotionTypes.PromotionDTO,
|
promotion: PromotionTypes.PromotionDTO,
|
||||||
@@ -61,10 +63,21 @@ export function applyPromotionToShippingMethods(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||||
|
promotion,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if (budgetExceededAction) {
|
||||||
|
computedActions.push(budgetExceededAction)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "addShippingMethodAdjustment",
|
action: ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||||
shipping_method_id: method.id,
|
shipping_method_id: method.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
@@ -99,10 +112,21 @@ export function applyPromotionToShippingMethods(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||||
|
promotion,
|
||||||
|
amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if (budgetExceededAction) {
|
||||||
|
computedActions.push(budgetExceededAction)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: "addShippingMethodAdjustment",
|
action: ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||||
shipping_method_id: method.id,
|
shipping_method_id: method.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
CampaignBudgetExceededAction,
|
||||||
|
ComputeActions,
|
||||||
|
PromotionDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { CampaignBudgetType, ComputedActions } from "@medusajs/utils"
|
||||||
|
|
||||||
|
export function canRegisterUsage(computedAction: ComputeActions): boolean {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||||
|
ComputedActions.ADD_SHIPPING_METHOD_ADJUSTMENT,
|
||||||
|
] as string[]
|
||||||
|
).includes(computedAction.action)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeActionForBudgetExceeded(
|
||||||
|
promotion: PromotionDTO,
|
||||||
|
amount: number
|
||||||
|
): CampaignBudgetExceededAction | void {
|
||||||
|
const campaignBudget = promotion.campaign?.budget
|
||||||
|
|
||||||
|
if (!campaignBudget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaignBudgetUsed = campaignBudget.used || 0
|
||||||
|
const totalUsed =
|
||||||
|
campaignBudget.type === CampaignBudgetType.SPEND
|
||||||
|
? campaignBudgetUsed + amount
|
||||||
|
: campaignBudgetUsed + 1
|
||||||
|
|
||||||
|
if (campaignBudget.limit && totalUsed > campaignBudget.limit) {
|
||||||
|
return {
|
||||||
|
action: ComputedActions.CAMPAIGN_BUDGET_EXCEEDED,
|
||||||
|
code: promotion.code!,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ApplicationMethodType,
|
ApplicationMethodType,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
isDefined,
|
isDefined,
|
||||||
|
isPresent,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
export const allowedAllocationTargetTypes: string[] = [
|
export const allowedAllocationTargetTypes: string[] = [
|
||||||
@@ -33,6 +34,16 @@ export function validateApplicationMethodAttributes(data: {
|
|||||||
}) {
|
}) {
|
||||||
const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType)
|
const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType)
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.allocation === ApplicationMethodAllocation.ACROSS &&
|
||||||
|
isPresent(data.max_quantity)
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`application_method.max_quantity is not allowed to be set for allocation (${ApplicationMethodAllocation.ACROSS})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!allTargetTypes.includes(data.target_type)) {
|
if (!allTargetTypes.includes(data.target_type)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export type CampaignBudgetTypeValues = "spend" | "usage"
|
|||||||
export interface CampaignBudgetDTO {
|
export interface CampaignBudgetDTO {
|
||||||
id: string
|
id: string
|
||||||
type?: CampaignBudgetTypeValues
|
type?: CampaignBudgetTypeValues
|
||||||
limit?: string | null
|
limit?: number | null
|
||||||
used?: string
|
used?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilterableCampaignBudgetProps
|
export interface FilterableCampaignBudgetProps
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ export type ComputeActions =
|
|||||||
| RemoveItemAdjustmentAction
|
| RemoveItemAdjustmentAction
|
||||||
| AddShippingMethodAdjustment
|
| AddShippingMethodAdjustment
|
||||||
| RemoveShippingMethodAdjustment
|
| RemoveShippingMethodAdjustment
|
||||||
|
| CampaignBudgetExceededAction
|
||||||
|
|
||||||
|
export type UsageComputedActions =
|
||||||
|
| AddShippingMethodAdjustment
|
||||||
|
| AddItemAdjustmentAction
|
||||||
|
|
||||||
|
export interface CampaignBudgetExceededAction {
|
||||||
|
action: "campaignBudgetExceeded"
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddItemAdjustmentAction {
|
export interface AddItemAdjustmentAction {
|
||||||
action: "addItemAdjustment"
|
action: "addItemAdjustment"
|
||||||
@@ -16,6 +26,7 @@ export interface RemoveItemAdjustmentAction {
|
|||||||
action: "removeItemAdjustment"
|
action: "removeItemAdjustment"
|
||||||
adjustment_id: string
|
adjustment_id: string
|
||||||
description?: string
|
description?: string
|
||||||
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddShippingMethodAdjustment {
|
export interface AddShippingMethodAdjustment {
|
||||||
@@ -29,6 +40,7 @@ export interface AddShippingMethodAdjustment {
|
|||||||
export interface RemoveShippingMethodAdjustment {
|
export interface RemoveShippingMethodAdjustment {
|
||||||
action: "removeShippingMethodAdjustment"
|
action: "removeShippingMethodAdjustment"
|
||||||
adjustment_id: string
|
adjustment_id: string
|
||||||
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputeActionAdjustmentLine extends Record<string, unknown> {
|
export interface ComputeActionAdjustmentLine extends Record<string, unknown> {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CreateApplicationMethodDTO,
|
CreateApplicationMethodDTO,
|
||||||
UpdateApplicationMethodDTO,
|
UpdateApplicationMethodDTO,
|
||||||
} from "./application-method"
|
} from "./application-method"
|
||||||
|
import { CampaignDTO } from "./campaign"
|
||||||
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
|
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
|
||||||
|
|
||||||
export type PromotionType = "standard" | "buyget"
|
export type PromotionType = "standard" | "buyget"
|
||||||
@@ -16,6 +17,7 @@ export interface PromotionDTO {
|
|||||||
is_automatic?: boolean
|
is_automatic?: boolean
|
||||||
application_method?: ApplicationMethodDTO
|
application_method?: ApplicationMethodDTO
|
||||||
rules?: PromotionRuleDTO[]
|
rules?: PromotionRuleDTO[]
|
||||||
|
campaign?: CampaignDTO
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePromotionDTO {
|
export interface CreatePromotionDTO {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
|
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
|
||||||
|
|
||||||
export interface IPromotionModuleService extends IModuleService {
|
export interface IPromotionModuleService extends IModuleService {
|
||||||
|
registerUsage(computedActions: ComputeActions[]): Promise<void>
|
||||||
|
|
||||||
computeActions(
|
computeActions(
|
||||||
promotionCodesToApply: string[],
|
promotionCodesToApply: string[],
|
||||||
applicationContext: ComputeActionContext,
|
applicationContext: ComputeActionContext,
|
||||||
|
|||||||
@@ -33,3 +33,11 @@ export enum CampaignBudgetType {
|
|||||||
SPEND = "spend",
|
SPEND = "spend",
|
||||||
USAGE = "usage",
|
USAGE = "usage",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ComputedActions {
|
||||||
|
ADD_ITEM_ADJUSTMENT = "addItemAdjustment",
|
||||||
|
ADD_SHIPPING_METHOD_ADJUSTMENT = "addShippingMethodAdjustment",
|
||||||
|
REMOVE_ITEM_ADJUSTMENT = "removeItemAdjustment",
|
||||||
|
REMOVE_SHIPPING_METHOD_ADJUSTMENT = "removeShippingMethodAdjustment",
|
||||||
|
CAMPAIGN_BUDGET_EXCEEDED = "campaignBudgetExceeded",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user