fix(promotion): handle promotion buy X get X scenario (#9002)
* fix(promotion): handle promotion buy X get X scenario * chore: fix qualifiication rules
This commit is contained in:
1 parent
e9b5f76f9a
commit
3593bdfebe
3 files changed
+568
-50
No files matched your search
+383
@@ -4689,6 +4689,389 @@ moduleIntegrationTestRunner({
|
|||||||
|
|
||||||
expect(JSON.parse(JSON.stringify(result))).toEqual([])
|
expect(JSON.parse(JSON.stringify(result))).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("when scenario is buy x get x", () => {
|
||||||
|
let buyXGetXPromotion
|
||||||
|
let product1 = "prod_tshirt_1"
|
||||||
|
let product2 = "prod_tshirt_2"
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
buyXGetXPromotion = await createDefaultPromotion(service, {
|
||||||
|
type: PromotionType.BUYGET,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
value: 2000,
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 2,
|
||||||
|
apply_to_quantity: 2,
|
||||||
|
buy_rules_min_quantity: 2,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buy_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compute adjustment accurately for a single item", async () => {
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 4,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt2",
|
||||||
|
quantity: 2,
|
||||||
|
subtotal: 2000,
|
||||||
|
product: { id: product2 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotion.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt",
|
||||||
|
amount: 500,
|
||||||
|
code: "PROMOTION_TEST",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compute adjustment accurately for a single item when multiple buyget promos are applied", async () => {
|
||||||
|
const buyXGetXPromotionBulk1 = await createDefaultPromotion(
|
||||||
|
service,
|
||||||
|
{
|
||||||
|
code: "BUY50GET1000",
|
||||||
|
type: PromotionType.BUYGET,
|
||||||
|
campaign_id: null,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
value: 20000,
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 1000,
|
||||||
|
apply_to_quantity: 1000,
|
||||||
|
buy_rules_min_quantity: 50,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buy_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const buyXGetXPromotionBulk2 = await createDefaultPromotion(
|
||||||
|
service,
|
||||||
|
{
|
||||||
|
code: "BUY10GET20",
|
||||||
|
type: PromotionType.BUYGET,
|
||||||
|
campaign_id: null,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
value: 20000,
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 20,
|
||||||
|
apply_to_quantity: 20,
|
||||||
|
buy_rules_min_quantity: 10,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buy_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 1080,
|
||||||
|
subtotal: 2700,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotionBulk1.code!, buyXGetXPromotionBulk2.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt",
|
||||||
|
amount: 2500,
|
||||||
|
code: "BUY50GET1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
amount: 50,
|
||||||
|
code: "BUY10GET20",
|
||||||
|
item_id: "item_cotton_tshirt",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compute adjustment accurately for multiple items when multiple buyget promos are applied", async () => {
|
||||||
|
const buyXGetXPromotionBulk1 = await createDefaultPromotion(
|
||||||
|
service,
|
||||||
|
{
|
||||||
|
code: "BUY50GET1000",
|
||||||
|
type: PromotionType.BUYGET,
|
||||||
|
campaign_id: null,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
value: 20000,
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 1000,
|
||||||
|
apply_to_quantity: 1000,
|
||||||
|
buy_rules_min_quantity: 50,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buy_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const buyXGetXPromotionBulk2 = await createDefaultPromotion(
|
||||||
|
service,
|
||||||
|
{
|
||||||
|
code: "BUY10GET20",
|
||||||
|
type: PromotionType.BUYGET,
|
||||||
|
campaign_id: null,
|
||||||
|
application_method: {
|
||||||
|
type: "fixed",
|
||||||
|
target_type: "items",
|
||||||
|
value: 20000,
|
||||||
|
allocation: "each",
|
||||||
|
max_quantity: 20,
|
||||||
|
apply_to_quantity: 20,
|
||||||
|
buy_rules_min_quantity: 10,
|
||||||
|
target_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
buy_rules: [
|
||||||
|
{
|
||||||
|
attribute: "product.id",
|
||||||
|
operator: "eq",
|
||||||
|
values: [product1],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 540,
|
||||||
|
subtotal: 1350,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt2",
|
||||||
|
quantity: 540,
|
||||||
|
subtotal: 1350,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotionBulk1.code!, buyXGetXPromotionBulk2.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt2",
|
||||||
|
amount: 1225,
|
||||||
|
code: "BUY50GET1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt",
|
||||||
|
amount: 1275,
|
||||||
|
code: "BUY50GET1000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt2",
|
||||||
|
amount: 50,
|
||||||
|
code: "BUY10GET20",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should compute adjustment accurately across items", async () => {
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 500,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt1",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 500,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt2",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt3",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotion.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt1",
|
||||||
|
amount: 500,
|
||||||
|
code: "PROMOTION_TEST",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: "addItemAdjustment",
|
||||||
|
item_id: "item_cotton_tshirt",
|
||||||
|
amount: 500,
|
||||||
|
code: "PROMOTION_TEST",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not compute adjustment when required quantity for target isn't met", async () => {
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 3,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotion.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not compute adjustment when required quantity for target isn't met across items", async () => {
|
||||||
|
const context = {
|
||||||
|
currency_code: "usd",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt1",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "item_cotton_tshirt2",
|
||||||
|
quantity: 1,
|
||||||
|
subtotal: 1000,
|
||||||
|
product: { id: product1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await service.computeActions(
|
||||||
|
[buyXGetXPromotion.code!],
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(JSON.parse(JSON.stringify(result))).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
validateApplicationMethodAttributes,
|
validateApplicationMethodAttributes,
|
||||||
validatePromotionRuleAttributes,
|
validatePromotionRuleAttributes,
|
||||||
} from "@utils"
|
} from "@utils"
|
||||||
|
import { EligibleItem } from "src/utils/compute-actions"
|
||||||
import { joinerConfig } from "../joiner-config"
|
import { joinerConfig } from "../joiner-config"
|
||||||
import { CreatePromotionRuleValueDTO } from "../types/promotion-rule-value"
|
import { CreatePromotionRuleValueDTO } from "../types/promotion-rule-value"
|
||||||
|
|
||||||
@@ -337,6 +338,10 @@ export default class PromotionModuleService
|
|||||||
PromotionTypes.ComputeActionAdjustmentLine[]
|
PromotionTypes.ComputeActionAdjustmentLine[]
|
||||||
>()
|
>()
|
||||||
const methodIdPromoValueMap = new Map<string, number>()
|
const methodIdPromoValueMap = new Map<string, number>()
|
||||||
|
// Keeps a map of all elgible items in the buy section and its eligible quantity
|
||||||
|
const eligibleBuyItemMap = new Map<string, EligibleItem[]>()
|
||||||
|
// Keeps a map of all elgible items in the target section and its eligible quantity
|
||||||
|
const eligibleTargetItemMap = new Map<string, EligibleItem[]>()
|
||||||
const automaticPromotions = preventAutoPromotions
|
const automaticPromotions = preventAutoPromotions
|
||||||
? []
|
? []
|
||||||
: await this.listPromotions(
|
: await this.listPromotions(
|
||||||
@@ -467,8 +472,10 @@ export default class PromotionModuleService
|
|||||||
const computedActionsForItems =
|
const computedActionsForItems =
|
||||||
ComputeActionUtils.getComputedActionsForBuyGet(
|
ComputeActionUtils.getComputedActionsForBuyGet(
|
||||||
promotion,
|
promotion,
|
||||||
applicationContext[ApplicationMethodTargetType.ITEMS],
|
applicationContext[ApplicationMethodTargetType.ITEMS]!,
|
||||||
methodIdPromoValueMap
|
methodIdPromoValueMap,
|
||||||
|
eligibleBuyItemMap,
|
||||||
|
eligibleTargetItemMap
|
||||||
)
|
)
|
||||||
|
|
||||||
computedActions.push(...computedActionsForItems)
|
computedActions.push(...computedActionsForItems)
|
||||||
|
|||||||
@@ -1,28 +1,63 @@
|
|||||||
import { BigNumberInput, PromotionTypes } from "@medusajs/types"
|
import {
|
||||||
|
BigNumberInput,
|
||||||
|
ComputeActionItemLine,
|
||||||
|
PromotionTypes,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
ComputedActions,
|
ComputedActions,
|
||||||
MathBN,
|
MathBN,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
PromotionType,
|
PromotionType,
|
||||||
isPresent,
|
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { areRulesValidForContext } from "../validations"
|
import { areRulesValidForContext } from "../validations"
|
||||||
import { computeActionForBudgetExceeded } from "./usage"
|
import { computeActionForBudgetExceeded } from "./usage"
|
||||||
|
|
||||||
// TODO: calculations should eventually move to a totals util outside of the module
|
export type EligibleItem = {
|
||||||
|
item_id: string
|
||||||
|
quantity: BigNumberInput
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByPrice(a: ComputeActionItemLine, b: ComputeActionItemLine) {
|
||||||
|
return MathBN.lt(a.subtotal, b.subtotal) ? 1 : -1
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Grabs all the items in the context where the rules apply
|
||||||
|
We then sort by price to prioritize most valuable item
|
||||||
|
*/
|
||||||
|
function filterItemsByPromotionRules(
|
||||||
|
itemsContext: ComputeActionItemLine[],
|
||||||
|
rules?: PromotionTypes.PromotionRuleDTO[]
|
||||||
|
) {
|
||||||
|
return itemsContext
|
||||||
|
.filter((item) =>
|
||||||
|
areRulesValidForContext(
|
||||||
|
rules || [],
|
||||||
|
item,
|
||||||
|
ApplicationMethodTargetType.ITEMS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sort(sortByPrice)
|
||||||
|
}
|
||||||
|
|
||||||
export function getComputedActionsForBuyGet(
|
export function getComputedActionsForBuyGet(
|
||||||
promotion: PromotionTypes.PromotionDTO,
|
promotion: PromotionTypes.PromotionDTO,
|
||||||
itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
|
itemsContext: ComputeActionItemLine[],
|
||||||
methodIdPromoValueMap: Map<string, BigNumberInput>
|
methodIdPromoValueMap: Map<string, BigNumberInput>,
|
||||||
|
eligibleBuyItemMap: Map<string, EligibleItem[]>,
|
||||||
|
eligibleTargetItemMap: Map<string, EligibleItem[]>
|
||||||
): PromotionTypes.ComputeActions[] {
|
): 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[] = []
|
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||||
|
|
||||||
|
const minimumBuyQuantity = MathBN.convert(
|
||||||
|
promotion.application_method?.buy_rules_min_quantity ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemsMap = new Map<string, ComputeActionItemLine>(
|
||||||
|
itemsContext.map((i) => [i.id, i])
|
||||||
|
)
|
||||||
|
|
||||||
if (!itemsContext) {
|
if (!itemsContext) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_DATA,
|
MedusaError.Types.INVALID_DATA,
|
||||||
@@ -30,56 +65,146 @@ export function getComputedActionsForBuyGet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(buyRules) || !Array.isArray(targetRules)) {
|
const eligibleBuyItems = filterItemsByPromotionRules(
|
||||||
|
itemsContext,
|
||||||
|
promotion.application_method?.buy_rules
|
||||||
|
)
|
||||||
|
|
||||||
|
const eligibleBuyItemQuantity = MathBN.sum(
|
||||||
|
...eligibleBuyItems.map((item) => item.quantity)
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the total quantity of items where buy rules apply. If the total sum of eligible items
|
||||||
|
does not match up to the minimum buy quantity set on the promotion, return early.
|
||||||
|
*/
|
||||||
|
if (MathBN.gt(minimumBuyQuantity, eligibleBuyItemQuantity)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const validQuantity = MathBN.sum(
|
/*
|
||||||
...itemsContext
|
Eligibility of a BuyGet promotion can span across line items. Once an item has been chosen
|
||||||
.filter((item) =>
|
as eligible, we can't use this item or its partial remaining quantity when we apply the promotion on
|
||||||
areRulesValidForContext(
|
the target item.
|
||||||
buyRules,
|
|
||||||
item,
|
We build the map here to use when we apply promotions on the target items.
|
||||||
ApplicationMethodTargetType.ITEMS
|
*/
|
||||||
)
|
for (const eligibleBuyItem of eligibleBuyItems) {
|
||||||
)
|
const eligibleItemsByPromotion =
|
||||||
.map((item) => item.quantity)
|
eligibleBuyItemMap.get(promotion.code!) || []
|
||||||
|
|
||||||
|
const accumulatedQuantity = eligibleItemsByPromotion.reduce(
|
||||||
|
(acc, item) => MathBN.sum(acc, item.quantity),
|
||||||
|
MathBN.convert(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
// If we have reached the minimum buy quantity from the eligible items for this promotion,
|
||||||
!buyRulesMinQuantity ||
|
// we can break early and continue to applying the target items
|
||||||
!applyToQuantity ||
|
if (MathBN.gte(accumulatedQuantity, minimumBuyQuantity)) {
|
||||||
MathBN.gt(buyRulesMinQuantity, validQuantity)
|
break
|
||||||
) {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validItemsForTargetRules = itemsContext
|
const eligibleQuantity = MathBN.sum(
|
||||||
.filter((item) =>
|
...eligibleItemsByPromotion
|
||||||
areRulesValidForContext(
|
.filter((buy) => buy.item_id === eligibleBuyItem.id)
|
||||||
targetRules,
|
.map((b) => b.quantity)
|
||||||
item,
|
|
||||||
ApplicationMethodTargetType.ITEMS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const reservableQuantity = MathBN.min(
|
||||||
|
eligibleBuyItem.quantity,
|
||||||
|
MathBN.sub(minimumBuyQuantity, eligibleQuantity)
|
||||||
)
|
)
|
||||||
.filter((item) => isPresent(item.subtotal) && isPresent(item.quantity))
|
|
||||||
.sort((a, b) => {
|
|
||||||
const divA = MathBN.eq(a.quantity, 0) ? 1 : a.quantity
|
|
||||||
const divB = MathBN.eq(b.quantity, 0) ? 1 : b.quantity
|
|
||||||
|
|
||||||
const aPrice = MathBN.div(a.subtotal, divA)
|
// If we have reached the required minimum quantity, we break the loop early
|
||||||
const bPrice = MathBN.div(b.subtotal, divB)
|
if (MathBN.lte(reservableQuantity, 0)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
return MathBN.lt(bPrice, aPrice) ? -1 : 1
|
eligibleItemsByPromotion.push({
|
||||||
|
item_id: eligibleBuyItem.id,
|
||||||
|
quantity: MathBN.min(
|
||||||
|
eligibleBuyItem.quantity,
|
||||||
|
reservableQuantity
|
||||||
|
).toNumber(),
|
||||||
})
|
})
|
||||||
|
|
||||||
let remainingQtyToApply = MathBN.convert(applyToQuantity)
|
eligibleBuyItemMap.set(promotion.code!, eligibleItemsByPromotion)
|
||||||
|
}
|
||||||
|
|
||||||
|
const eligibleTargetItems = filterItemsByPromotionRules(
|
||||||
|
itemsContext,
|
||||||
|
promotion.application_method?.target_rules
|
||||||
|
)
|
||||||
|
|
||||||
|
const targetQuantity = MathBN.convert(
|
||||||
|
promotion.application_method?.apply_to_quantity ?? 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this loop, we build a map of eligible target items and quantity applicable to these items.
|
||||||
|
|
||||||
|
Here we remove the quantity we used previously to identify eligible buy items
|
||||||
|
from the eligible target items.
|
||||||
|
|
||||||
|
This is done to prevent applying promotion to the same item we use to qualify the buy rules.
|
||||||
|
*/
|
||||||
|
for (const eligibleTargetItem of eligibleTargetItems) {
|
||||||
|
const inapplicableQuantity = MathBN.sum(
|
||||||
|
...Array.from(eligibleBuyItemMap.values())
|
||||||
|
.flat(1)
|
||||||
|
.filter((buy) => buy.item_id === eligibleTargetItem.id)
|
||||||
|
.map((b) => b.quantity)
|
||||||
|
)
|
||||||
|
|
||||||
|
const applicableQuantity = MathBN.sub(
|
||||||
|
eligibleTargetItem.quantity,
|
||||||
|
inapplicableQuantity
|
||||||
|
)
|
||||||
|
|
||||||
|
const fulfillableQuantity = MathBN.min(targetQuantity, applicableQuantity)
|
||||||
|
|
||||||
|
// If we have reached the required quantity to target from this item, we
|
||||||
|
// move on to the next item
|
||||||
|
if (MathBN.lte(fulfillableQuantity, 0)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetItemsByPromotion =
|
||||||
|
eligibleTargetItemMap.get(promotion.code!) || []
|
||||||
|
|
||||||
|
targetItemsByPromotion.push({
|
||||||
|
item_id: eligibleTargetItem.id,
|
||||||
|
quantity: MathBN.min(fulfillableQuantity, targetQuantity).toNumber(),
|
||||||
|
})
|
||||||
|
|
||||||
|
eligibleTargetItemMap.set(promotion.code!, targetItemsByPromotion)
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetItemsByPromotion =
|
||||||
|
eligibleTargetItemMap.get(promotion.code!) || []
|
||||||
|
|
||||||
|
const targettableQuantity = targetItemsByPromotion.reduce(
|
||||||
|
(sum, item) => MathBN.sum(sum, item.quantity),
|
||||||
|
MathBN.convert(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we were able to match the target requirements across all line items, we return early.
|
||||||
|
if (MathBN.lt(targettableQuantity, targetQuantity)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let remainingQtyToApply = MathBN.convert(targetQuantity)
|
||||||
|
|
||||||
|
for (const targetItem of targetItemsByPromotion) {
|
||||||
|
const item = itemsMap.get(targetItem.item_id)!
|
||||||
|
const appliedPromoValue =
|
||||||
|
methodIdPromoValueMap.get(item.id) ?? MathBN.convert(0)
|
||||||
|
const multiplier = MathBN.min(targetItem.quantity, remainingQtyToApply)
|
||||||
|
const amount = MathBN.mult(
|
||||||
|
MathBN.div(item.subtotal, item.quantity),
|
||||||
|
multiplier
|
||||||
|
)
|
||||||
|
|
||||||
for (const method of validItemsForTargetRules) {
|
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
|
||||||
const multiplier = MathBN.min(method.quantity, remainingQtyToApply)
|
|
||||||
const div = MathBN.eq(method.quantity, 0) ? 1 : method.quantity
|
|
||||||
const amount = MathBN.mult(MathBN.div(method.subtotal, div), multiplier)
|
|
||||||
const newRemainingQtyToApply = MathBN.sub(remainingQtyToApply, multiplier)
|
const newRemainingQtyToApply = MathBN.sub(remainingQtyToApply, multiplier)
|
||||||
|
|
||||||
if (MathBN.lt(newRemainingQtyToApply, 0) || MathBN.lte(amount, 0)) {
|
if (MathBN.lt(newRemainingQtyToApply, 0) || MathBN.lte(amount, 0)) {
|
||||||
@@ -99,11 +224,14 @@ export function getComputedActionsForBuyGet(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
methodIdPromoValueMap.set(method.id, MathBN.add(appliedPromoValue, amount))
|
methodIdPromoValueMap.set(
|
||||||
|
item.id,
|
||||||
|
MathBN.add(appliedPromoValue, amount).toNumber()
|
||||||
|
)
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||||
item_id: method.id,
|
item_id: item.id,
|
||||||
amount,
|
amount,
|
||||||
code: promotion.code!,
|
code: promotion.code!,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in new issue
Block a user