feat(): added percentage calculations for all cases (#6300)

what:

- adds missing percentage calculations for items and shipping methods
This commit is contained in:
Riqwan Thamir
2024-02-02 11:39:34 +00:00
committed by GitHub
parent 9fda6a6824
commit abc30517cb
7 changed files with 3711 additions and 1735 deletions
@@ -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[]
} }