feat(): added percentage calculations for all cases (#6300)
what: - adds missing percentage calculations for items and shipping methods
This commit is contained in:
+3622
-1713
File diff suppressed because it is too large
Load Diff
+24
-5
@@ -1,16 +1,17 @@
|
|||||||
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { IPromotionModuleService } from "@medusajs/types"
|
import { IPromotionModuleService } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
|
ApplicationMethodTargetType,
|
||||||
ApplicationMethodType,
|
ApplicationMethodType,
|
||||||
CampaignBudgetType,
|
CampaignBudgetType,
|
||||||
PromotionType,
|
PromotionType,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
|
import { initModules } from "medusa-test-utils"
|
||||||
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
import { createCampaigns } from "../../../__fixtures__/campaigns"
|
||||||
import { createPromotions } from "../../../__fixtures__/promotion"
|
import { createPromotions } from "../../../__fixtures__/promotion"
|
||||||
import { MikroOrmWrapper } from "../../../utils"
|
import { MikroOrmWrapper } from "../../../utils"
|
||||||
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
|
import { getInitModuleConfig } from "../../../utils/get-init-module-config"
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
|
||||||
import { initModules } from "medusa-test-utils/dist"
|
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -114,6 +115,24 @@ describe("Promotion Service", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should throw error when percentage type and value is greater than 100", async () => {
|
||||||
|
const error = await service
|
||||||
|
.create({
|
||||||
|
code: "PROMOTION_TEST",
|
||||||
|
type: PromotionType.STANDARD,
|
||||||
|
application_method: {
|
||||||
|
type: ApplicationMethodType.PERCENTAGE,
|
||||||
|
target_type: ApplicationMethodTargetType.ORDER,
|
||||||
|
value: "1000",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.message).toContain(
|
||||||
|
"Application Method value should be a percentage number between 0 and 100"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it("should throw an error when both campaign and campaign_id are provided", async () => {
|
it("should throw an error when both campaign and campaign_id are provided", async () => {
|
||||||
const startsAt = new Date("01/01/2023")
|
const startsAt = new Date("01/01/2023")
|
||||||
const endsAt = new Date("01/01/2023")
|
const endsAt = new Date("01/01/2023")
|
||||||
@@ -655,7 +674,7 @@ describe("Promotion Service", () => {
|
|||||||
is_automatic: true,
|
is_automatic: true,
|
||||||
code: "TEST",
|
code: "TEST",
|
||||||
type: PromotionType.BUYGET,
|
type: PromotionType.BUYGET,
|
||||||
},
|
} as any,
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(updatedPromotion).toEqual(
|
expect(updatedPromotion).toEqual(
|
||||||
@@ -899,12 +918,12 @@ describe("Promotion Service", () => {
|
|||||||
value: "200",
|
value: "200",
|
||||||
target_type: "items",
|
target_type: "items",
|
||||||
},
|
},
|
||||||
},
|
} as any,
|
||||||
{
|
{
|
||||||
id: "promotion-id-2",
|
id: "promotion-id-2",
|
||||||
code: "PROMOTION_2",
|
code: "PROMOTION_2",
|
||||||
type: PromotionType.STANDARD,
|
type: PromotionType.STANDARD,
|
||||||
},
|
} as any,
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export function getComputedActionsForBuyGet(
|
|||||||
const validItemsForTargetRules = itemsContext
|
const validItemsForTargetRules = itemsContext
|
||||||
.filter((item) => areRulesValidForContext(targetRules, item))
|
.filter((item) => areRulesValidForContext(targetRules, item))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return b.unit_price - a.unit_price
|
const aPrice = a.subtotal / a.quantity
|
||||||
|
const bPrice = b.subtotal / b.quantity
|
||||||
|
|
||||||
|
return bPrice - aPrice
|
||||||
})
|
})
|
||||||
|
|
||||||
let remainingQtyToApply = applyToQuantity
|
let remainingQtyToApply = applyToQuantity
|
||||||
@@ -54,7 +57,7 @@ export function getComputedActionsForBuyGet(
|
|||||||
for (const method of validItemsForTargetRules) {
|
for (const method of validItemsForTargetRules) {
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
const multiplier = Math.min(method.quantity, remainingQtyToApply)
|
const multiplier = Math.min(method.quantity, remainingQtyToApply)
|
||||||
const amount = method.unit_price * multiplier
|
const amount = (method.subtotal / method.quantity) * multiplier
|
||||||
const newRemainingQtyToApply = remainingQtyToApply - multiplier
|
const newRemainingQtyToApply = remainingQtyToApply - multiplier
|
||||||
|
|
||||||
if (newRemainingQtyToApply < 0 || amount <= 0) {
|
if (newRemainingQtyToApply < 0 || amount <= 0) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ApplicationMethodAllocation,
|
ApplicationMethodAllocation,
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
|
ApplicationMethodType,
|
||||||
ComputedActions,
|
ComputedActions,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -67,10 +68,15 @@ export function applyPromotionToItems(
|
|||||||
method.quantity,
|
method.quantity,
|
||||||
applicationMethod?.max_quantity!
|
applicationMethod?.max_quantity!
|
||||||
)
|
)
|
||||||
const promotionValue =
|
const totalItemValue =
|
||||||
parseFloat(applicationMethod!.value!) * quantityMultiplier
|
(method.subtotal / method.quantity) * quantityMultiplier
|
||||||
const applicableTotal =
|
let promotionValue = parseFloat(applicationMethod!.value!)
|
||||||
method.unit_price * quantityMultiplier - appliedPromoValue
|
const applicableTotal = totalItemValue - appliedPromoValue
|
||||||
|
|
||||||
|
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
|
promotionValue = (promotionValue / 100) * applicableTotal
|
||||||
|
}
|
||||||
|
|
||||||
const amount = Math.min(promotionValue, applicableTotal)
|
const amount = Math.min(promotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -106,18 +112,32 @@ 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 acc + method.unit_price * method.quantity - appliedPromoValue
|
return (
|
||||||
|
acc +
|
||||||
|
(method.subtotal / method.quantity) * method.quantity -
|
||||||
|
appliedPromoValue
|
||||||
|
)
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
for (const method of items!) {
|
for (const method of items!) {
|
||||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
|
const promotionValue = parseFloat(applicationMethod!.value!)
|
||||||
const applicableTotal =
|
const applicableTotal =
|
||||||
method.unit_price * method.quantity - appliedPromoValue
|
(method.subtotal / method.quantity) * method.quantity -
|
||||||
|
appliedPromoValue
|
||||||
|
|
||||||
|
if (applicableTotal <= 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: should we worry about precision here?
|
// TODO: should we worry about precision here?
|
||||||
const applicablePromotionValue =
|
let applicablePromotionValue =
|
||||||
(applicableTotal / totalApplicableValue) * promotionValue
|
(applicableTotal / totalApplicableValue) * promotionValue
|
||||||
|
|
||||||
|
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
|
applicablePromotionValue = (promotionValue / 100) * applicableTotal
|
||||||
|
}
|
||||||
|
|
||||||
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -135,6 +155,8 @@ export function applyPromotionToItems(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
|
||||||
|
|
||||||
computedActions.push({
|
computedActions.push({
|
||||||
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
action: ComputedActions.ADD_ITEM_ADJUSTMENT,
|
||||||
item_id: method.id,
|
item_id: method.id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { PromotionTypes } from "@medusajs/types"
|
|||||||
import {
|
import {
|
||||||
ApplicationMethodAllocation,
|
ApplicationMethodAllocation,
|
||||||
ApplicationMethodTargetType,
|
ApplicationMethodTargetType,
|
||||||
|
ApplicationMethodType,
|
||||||
ComputedActions,
|
ComputedActions,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
@@ -55,8 +56,13 @@ export function applyPromotionToShippingMethods(
|
|||||||
if (allocation === ApplicationMethodAllocation.EACH) {
|
if (allocation === ApplicationMethodAllocation.EACH) {
|
||||||
for (const method of shippingMethods!) {
|
for (const method of shippingMethods!) {
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
let promotionValue = parseFloat(applicationMethod!.value!)
|
||||||
const applicableTotal = method.unit_price - appliedPromoValue
|
const applicableTotal = method.subtotal - appliedPromoValue
|
||||||
|
|
||||||
|
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
|
promotionValue = (promotionValue / 100) * applicableTotal
|
||||||
|
}
|
||||||
|
|
||||||
const amount = Math.min(promotionValue, applicableTotal)
|
const amount = Math.min(promotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
@@ -89,7 +95,7 @@ export function applyPromotionToShippingMethods(
|
|||||||
const totalApplicableValue = shippingMethods!.reduce((acc, method) => {
|
const totalApplicableValue = shippingMethods!.reduce((acc, method) => {
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
|
|
||||||
return acc + method.unit_price - appliedPromoValue
|
return acc + method.subtotal - appliedPromoValue
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
if (totalApplicableValue <= 0) {
|
if (totalApplicableValue <= 0) {
|
||||||
@@ -98,14 +104,19 @@ export function applyPromotionToShippingMethods(
|
|||||||
|
|
||||||
for (const method of shippingMethods!) {
|
for (const method of shippingMethods!) {
|
||||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
const promotionValue = parseFloat(applicationMethod!.value!)
|
||||||
const applicableTotal = method.unit_price
|
const applicableTotal = method.subtotal
|
||||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||||
|
|
||||||
// TODO: should we worry about precision here?
|
// TODO: should we worry about precision here?
|
||||||
const applicablePromotionValue =
|
let applicablePromotionValue =
|
||||||
(applicableTotal / totalApplicableValue) * promotionValue -
|
(applicableTotal / totalApplicableValue) * promotionValue -
|
||||||
appliedPromoValue
|
appliedPromoValue
|
||||||
|
|
||||||
|
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
|
||||||
|
applicablePromotionValue =
|
||||||
|
(promotionValue / 100) * (applicableTotal - appliedPromoValue)
|
||||||
|
}
|
||||||
|
|
||||||
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
const amount = Math.min(applicablePromotionValue, applicableTotal)
|
||||||
|
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
|
|||||||
@@ -37,11 +37,23 @@ export function validateApplicationMethodAttributes(
|
|||||||
const applyToQuantity =
|
const applyToQuantity =
|
||||||
data.apply_to_quantity || applicationMethod?.apply_to_quantity
|
data.apply_to_quantity || applicationMethod?.apply_to_quantity
|
||||||
const targetType = data.target_type || applicationMethod?.target_type
|
const targetType = data.target_type || applicationMethod?.target_type
|
||||||
|
const type = data.type || applicationMethod?.type
|
||||||
const applicationMethodType = data.type || applicationMethod?.type
|
const applicationMethodType = data.type || applicationMethod?.type
|
||||||
|
const value = parseFloat(data.value! || applicationMethod?.value!)
|
||||||
const maxQuantity = data.max_quantity || applicationMethod.max_quantity
|
const maxQuantity = data.max_quantity || applicationMethod.max_quantity
|
||||||
const allocation = data.allocation || applicationMethod.allocation
|
const allocation = data.allocation || applicationMethod.allocation
|
||||||
const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType)
|
const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType)
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === ApplicationMethodType.PERCENTAGE &&
|
||||||
|
(value <= 0 || value > 100)
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Application Method value should be a percentage number between 0 and 100`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (promotion?.type === PromotionType.BUYGET) {
|
if (promotion?.type === PromotionType.BUYGET) {
|
||||||
if (!isPresent(applyToQuantity)) {
|
if (!isPresent(applyToQuantity)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
|
|||||||
@@ -51,13 +51,13 @@ export interface ComputeActionAdjustmentLine extends Record<string, unknown> {
|
|||||||
export interface ComputeActionItemLine extends Record<string, unknown> {
|
export interface ComputeActionItemLine extends Record<string, unknown> {
|
||||||
id: string
|
id: string
|
||||||
quantity: number
|
quantity: number
|
||||||
unit_price: number
|
subtotal: number
|
||||||
adjustments?: ComputeActionAdjustmentLine[]
|
adjustments?: ComputeActionAdjustmentLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ComputeActionShippingLine extends Record<string, unknown> {
|
export interface ComputeActionShippingLine extends Record<string, unknown> {
|
||||||
id: string
|
id: string
|
||||||
unit_price: number
|
subtotal: number
|
||||||
adjustments?: ComputeActionAdjustmentLine[]
|
adjustments?: ComputeActionAdjustmentLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user