feat(): added compute actions for buyget promotions (#6255)
what: - computes actions for buyget promotion (RESOLVES CORE-1700)
This commit is contained in:
@@ -38,44 +38,40 @@ describe("Promotion Service: computeActions", () => {
|
||||
})
|
||||
|
||||
describe("when code is not present in database", () => {
|
||||
it("should throw error when code in promotions array does not exist", async () => {
|
||||
const error = await service
|
||||
.computeActions(["DOES_NOT_EXIST"], {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
it("should return empty array when promotion does not exist", async () => {
|
||||
const response = await service.computeActions(["DOES_NOT_EXIST"], {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "item_cotton_tshirt",
|
||||
quantity: 1,
|
||||
unit_price: 100,
|
||||
product_category: {
|
||||
id: "catg_cotton",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt",
|
||||
},
|
||||
},
|
||||
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",
|
||||
},
|
||||
{
|
||||
id: "item_cotton_sweater",
|
||||
quantity: 5,
|
||||
unit_price: 150,
|
||||
product_category: {
|
||||
id: "catg_cotton",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater",
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((e) => e)
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(error.message).toContain(
|
||||
"Promotion for code (DOES_NOT_EXIST) not found"
|
||||
)
|
||||
expect(response).toEqual([])
|
||||
})
|
||||
|
||||
it("should throw error when code in items adjustment does not exist", async () => {
|
||||
@@ -2315,4 +2311,366 @@ describe("Promotion Service: computeActions", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("when promotion of type buyget", () => {
|
||||
it("should compute adjustment when target and buy rules match", async () => {
|
||||
const context = {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "item_cotton_tshirt",
|
||||
quantity: 2,
|
||||
unit_price: 500,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_tshirt2",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_sweater",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_sweater",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater_1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const [createdPromotion] = await service.create([
|
||||
{
|
||||
code: "PROMOTION_TEST",
|
||||
type: PromotionType.BUYGET,
|
||||
rules: [
|
||||
{
|
||||
attribute: "customer.customer_group.id",
|
||||
operator: "in",
|
||||
values: ["VIP", "top100"],
|
||||
},
|
||||
],
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "each",
|
||||
max_quantity: 1,
|
||||
apply_to_quantity: 1,
|
||||
buy_rules_min_quantity: 1,
|
||||
target_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_tshirt"],
|
||||
},
|
||||
],
|
||||
buy_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_sweater"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.computeActions(["PROMOTION_TEST"], context)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
action: "addItemAdjustment",
|
||||
item_id: "item_cotton_tshirt2",
|
||||
amount: 1000,
|
||||
code: "PROMOTION_TEST",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should return empty array when conditions for minimum qty aren't met", async () => {
|
||||
const context = {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "item_cotton_tshirt",
|
||||
quantity: 2,
|
||||
unit_price: 500,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_tshirt2",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_sweater",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_sweater",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater_1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const [createdPromotion] = await service.create([
|
||||
{
|
||||
code: "PROMOTION_TEST",
|
||||
type: PromotionType.BUYGET,
|
||||
rules: [
|
||||
{
|
||||
attribute: "customer.customer_group.id",
|
||||
operator: "in",
|
||||
values: ["VIP", "top100"],
|
||||
},
|
||||
],
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "each",
|
||||
max_quantity: 1,
|
||||
apply_to_quantity: 1,
|
||||
buy_rules_min_quantity: 4,
|
||||
target_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_tshirt"],
|
||||
},
|
||||
],
|
||||
buy_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_sweater"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.computeActions(["PROMOTION_TEST"], context)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it("should compute actions for multiple items when conditions for target qty exceed one item", async () => {
|
||||
const context = {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "item_cotton_tshirt",
|
||||
quantity: 2,
|
||||
unit_price: 500,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_tshirt2",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_sweater",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_sweater",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater_1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const [createdPromotion] = await service.create([
|
||||
{
|
||||
code: "PROMOTION_TEST",
|
||||
type: PromotionType.BUYGET,
|
||||
rules: [
|
||||
{
|
||||
attribute: "customer.customer_group.id",
|
||||
operator: "in",
|
||||
values: ["VIP", "top100"],
|
||||
},
|
||||
],
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "each",
|
||||
max_quantity: 1,
|
||||
apply_to_quantity: 4,
|
||||
buy_rules_min_quantity: 1,
|
||||
target_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_tshirt"],
|
||||
},
|
||||
],
|
||||
buy_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_sweater"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.computeActions(["PROMOTION_TEST"], context)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
action: "addItemAdjustment",
|
||||
item_id: "item_cotton_tshirt2",
|
||||
amount: 2000,
|
||||
code: "PROMOTION_TEST",
|
||||
},
|
||||
{
|
||||
action: "addItemAdjustment",
|
||||
item_id: "item_cotton_tshirt",
|
||||
amount: 1000,
|
||||
code: "PROMOTION_TEST",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("should return empty array when target rules arent met with context", async () => {
|
||||
const context = {
|
||||
customer: {
|
||||
customer_group: {
|
||||
id: "VIP",
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
id: "item_cotton_tshirt",
|
||||
quantity: 2,
|
||||
unit_price: 500,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_1",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_tshirt2",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_tshirt",
|
||||
},
|
||||
product: {
|
||||
id: "prod_tshirt_2",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "item_cotton_sweater",
|
||||
quantity: 2,
|
||||
unit_price: 1000,
|
||||
product_category: {
|
||||
id: "catg_sweater",
|
||||
},
|
||||
product: {
|
||||
id: "prod_sweater_1",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const [createdPromotion] = await service.create([
|
||||
{
|
||||
code: "PROMOTION_TEST",
|
||||
type: PromotionType.BUYGET,
|
||||
rules: [
|
||||
{
|
||||
attribute: "customer.customer_group.id",
|
||||
operator: "in",
|
||||
values: ["VIP", "top100"],
|
||||
},
|
||||
],
|
||||
application_method: {
|
||||
type: "fixed",
|
||||
target_type: "items",
|
||||
allocation: "each",
|
||||
max_quantity: 1,
|
||||
apply_to_quantity: 4,
|
||||
buy_rules_min_quantity: 1,
|
||||
target_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_not-found"],
|
||||
},
|
||||
],
|
||||
buy_rules: [
|
||||
{
|
||||
attribute: "product_category.id",
|
||||
operator: "eq",
|
||||
values: ["catg_sweater"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.computeActions(["PROMOTION_TEST"], context)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -263,6 +263,8 @@ export default class PromotionModuleService<
|
||||
"application_method",
|
||||
"application_method.target_rules",
|
||||
"application_method.target_rules.values",
|
||||
"application_method.buy_rules",
|
||||
"application_method.buy_rules.values",
|
||||
"rules",
|
||||
"rules.values",
|
||||
"campaign",
|
||||
@@ -271,6 +273,10 @@ export default class PromotionModuleService<
|
||||
}
|
||||
)
|
||||
|
||||
const sortedPermissionsToApply = promotions
|
||||
.filter((p) => promotionCodesToApply.includes(p.code!))
|
||||
.sort(ComputeActionUtils.sortByBuyGetType)
|
||||
|
||||
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
|
||||
promotions.map((promotion) => [promotion.code!, promotion])
|
||||
)
|
||||
@@ -306,15 +312,8 @@ export default class PromotionModuleService<
|
||||
}
|
||||
}
|
||||
|
||||
for (const promotionCode of promotionCodesToApply) {
|
||||
const promotion = existingPromotionsMap.get(promotionCode)
|
||||
|
||||
if (!promotion) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Promotion for code (${promotionCode}) not found`
|
||||
)
|
||||
}
|
||||
for (const promotionToApply of sortedPermissionsToApply) {
|
||||
const promotion = existingPromotionsMap.get(promotionToApply.code!)!
|
||||
|
||||
const {
|
||||
application_method: applicationMethod,
|
||||
@@ -334,20 +333,9 @@ export default class PromotionModuleService<
|
||||
continue
|
||||
}
|
||||
|
||||
if (applicationMethod.target_type === ApplicationMethodTargetType.ORDER) {
|
||||
if (promotion.type === PromotionType.BUYGET) {
|
||||
const computedActionsForItems =
|
||||
ComputeActionUtils.getComputedActionsForOrder(
|
||||
promotion,
|
||||
applicationContext,
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
|
||||
computedActions.push(...computedActionsForItems)
|
||||
}
|
||||
|
||||
if (applicationMethod.target_type === ApplicationMethodTargetType.ITEMS) {
|
||||
const computedActionsForItems =
|
||||
ComputeActionUtils.getComputedActionsForItems(
|
||||
ComputeActionUtils.getComputedActionsForBuyGet(
|
||||
promotion,
|
||||
applicationContext[ApplicationMethodTargetType.ITEMS],
|
||||
methodIdPromoValueMap
|
||||
@@ -356,18 +344,46 @@ export default class PromotionModuleService<
|
||||
computedActions.push(...computedActionsForItems)
|
||||
}
|
||||
|
||||
if (
|
||||
applicationMethod.target_type ===
|
||||
ApplicationMethodTargetType.SHIPPING_METHODS
|
||||
) {
|
||||
const computedActionsForShippingMethods =
|
||||
ComputeActionUtils.getComputedActionsForShippingMethods(
|
||||
promotion,
|
||||
applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS],
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
if (promotion.type === PromotionType.STANDARD) {
|
||||
if (
|
||||
applicationMethod.target_type === ApplicationMethodTargetType.ORDER
|
||||
) {
|
||||
const computedActionsForItems =
|
||||
ComputeActionUtils.getComputedActionsForOrder(
|
||||
promotion,
|
||||
applicationContext,
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
|
||||
computedActions.push(...computedActionsForShippingMethods)
|
||||
computedActions.push(...computedActionsForItems)
|
||||
}
|
||||
|
||||
if (
|
||||
applicationMethod.target_type === ApplicationMethodTargetType.ITEMS
|
||||
) {
|
||||
const computedActionsForItems =
|
||||
ComputeActionUtils.getComputedActionsForItems(
|
||||
promotion,
|
||||
applicationContext[ApplicationMethodTargetType.ITEMS],
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
|
||||
computedActions.push(...computedActionsForItems)
|
||||
}
|
||||
|
||||
if (
|
||||
applicationMethod.target_type ===
|
||||
ApplicationMethodTargetType.SHIPPING_METHODS
|
||||
) {
|
||||
const computedActionsForShippingMethods =
|
||||
ComputeActionUtils.getComputedActionsForShippingMethods(
|
||||
promotion,
|
||||
applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS],
|
||||
methodIdPromoValueMap
|
||||
)
|
||||
|
||||
computedActions.push(...computedActionsForShippingMethods)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
101
packages/promotion/src/utils/compute-actions/buy-get.ts
Normal file
101
packages/promotion/src/utils/compute-actions/buy-get.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { PromotionTypes } from "@medusajs/types"
|
||||
import {
|
||||
ApplicationMethodTargetType,
|
||||
ComputedActions,
|
||||
MedusaError,
|
||||
PromotionType,
|
||||
} from "@medusajs/utils"
|
||||
import { areRulesValidForContext } from "../validations/promotion-rule"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
|
||||
export function getComputedActionsForBuyGet(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
|
||||
methodIdPromoValueMap: Map<string, number>
|
||||
): PromotionTypes.ComputeActions[] {
|
||||
const buyRulesMinQuantity =
|
||||
promotion.application_method?.buy_rules_min_quantity
|
||||
const applyToQuantity = promotion.application_method?.apply_to_quantity
|
||||
const buyRules = promotion.application_method?.buy_rules
|
||||
const targetRules = promotion.application_method?.target_rules
|
||||
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||
|
||||
if (!itemsContext) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`"items" should be present as an array in the context to compute actions`
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(buyRules) || !Array.isArray(targetRules)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const validQuantity = itemsContext
|
||||
.filter((item) => areRulesValidForContext(buyRules, item))
|
||||
.reduce((acc, next) => acc + next.quantity, 0)
|
||||
|
||||
if (
|
||||
!buyRulesMinQuantity ||
|
||||
!applyToQuantity ||
|
||||
buyRulesMinQuantity > validQuantity
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
const validItemsForTargetRules = itemsContext
|
||||
.filter((item) => areRulesValidForContext(targetRules, item))
|
||||
.sort((a, b) => {
|
||||
return b.unit_price - a.unit_price
|
||||
})
|
||||
|
||||
let remainingQtyToApply = applyToQuantity
|
||||
|
||||
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 newRemainingQtyToApply = remainingQtyToApply - multiplier
|
||||
|
||||
if (newRemainingQtyToApply < 0 || amount <= 0) {
|
||||
break
|
||||
} else {
|
||||
remainingQtyToApply = newRemainingQtyToApply
|
||||
}
|
||||
|
||||
const budgetExceededAction = computeActionForBudgetExceeded(
|
||||
promotion,
|
||||
amount
|
||||
)
|
||||
|
||||
if (budgetExceededAction) {
|
||||
computedActions.push(budgetExceededAction)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||
|
||||
computedActions.push({
|
||||
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||
item_id: method.id,
|
||||
amount,
|
||||
code: promotion.code!,
|
||||
})
|
||||
}
|
||||
|
||||
return computedActions
|
||||
}
|
||||
|
||||
export function sortByBuyGetType(a, b) {
|
||||
if (a.type === PromotionType.BUYGET && b.type !== PromotionType.BUYGET) {
|
||||
return -1
|
||||
} else if (
|
||||
a.type !== PromotionType.BUYGET &&
|
||||
b.type === PromotionType.BUYGET
|
||||
) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./buy-get"
|
||||
export * from "./items"
|
||||
export * from "./order"
|
||||
export * from "./shipping-methods"
|
||||
|
||||
Reference in New Issue
Block a user