Chore(): localised improvement to promotion module (#13446)

**What**
- Remove overserialization withing transaction and rely more on internal service or protected method instead which does not serialize and work with Infered entity type
- Slightly rework loop complexity

Overall, this gives a good spare of resources and time spent for serialization
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2025-09-10 10:20:31 +00:00
1 parent d978749603
commit ac09b3cbef
9 files changed
+129 -67

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/promotion": patch
---
Chore/localised improvement to promotion module
+1 -1
View File
@@ -29,7 +29,7 @@
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
"build": "rimraf dist && tsc --build && npm run resolve:aliases", "build": "rimraf dist && tsc --build && npm run resolve:aliases",
"test": "jest --runInBand --passWithNoTests --bail --forceExit -- src", "test": "jest --runInBand --passWithNoTests --bail --forceExit -- src",
"test:integration": "jest --forceExit ", "test:integration": "jest --forceExit -- integration-tests/__tests__/**/*.spec.ts",
"migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial", "migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial",
"migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create", "migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
"migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up", "migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
@@ -139,11 +139,26 @@ export default class PromotionModuleService
} }
@InjectManager() @InjectManager()
listActivePromotions( async listActivePromotions(
filters?: FilterablePromotionProps, filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>, config?: FindConfig<PromotionDTO>,
sharedContext?: Context sharedContext?: Context
): Promise<PromotionDTO[]> { ): Promise<PromotionDTO[]> {
const activePromotions = await this.listActivePromotions_(
filters,
config,
sharedContext
)
return this.baseRepository_.serialize<PromotionDTO[]>(activePromotions)
}
@InjectManager()
protected async listActivePromotions_(
filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>,
@MedusaContext() sharedContext?: Context
): Promise<InferEntityType<typeof Promotion>[]> {
// Ensure we share the same now date across all filters // Ensure we share the same now date across all filters
const now = new Date() const now = new Date()
const activeFilters = { const activeFilters = {
@@ -170,7 +185,11 @@ export default class PromotionModuleService
], ],
} }
return this.listPromotions(activeFilters, config, sharedContext) return await this.promotionService_.list(
activeFilters,
config,
sharedContext
)
} }
@InjectTransactionManager() @InjectTransactionManager()
@@ -185,7 +204,7 @@ export default class PromotionModuleService
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>() const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const promotionCodeUsageMap = new Map<string, boolean>() const promotionCodeUsageMap = new Map<string, boolean>()
const existingPromotions = await this.listActivePromotions( const existingPromotions = await this.listActivePromotions_(
{ code: promotionCodes }, { code: promotionCodes },
{ relations: ["campaign", "campaign.budget"] }, { relations: ["campaign", "campaign.budget"] },
sharedContext sharedContext
@@ -200,9 +219,10 @@ export default class PromotionModuleService
} }
} }
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>( const existingPromotionsMap = new Map<
existingPromotions.map((promotion) => [promotion.code!, promotion]) string,
) InferEntityType<typeof Promotion>
>(existingPromotions.map((promotion) => [promotion.code!, promotion]))
for (let computedAction of computedActions) { for (let computedAction of computedActions) {
const promotion = existingPromotionsMap.get(computedAction.code) const promotion = existingPromotionsMap.get(computedAction.code)
@@ -293,7 +313,7 @@ export default class PromotionModuleService
const promotionCodeUsageMap = new Map<string, boolean>() const promotionCodeUsageMap = new Map<string, boolean>()
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>() const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const existingPromotions = await this.listActivePromotions( const existingPromotions = await this.listActivePromotions_(
{ {
code: computedActions code: computedActions
.map((computedAction) => computedAction.code) .map((computedAction) => computedAction.code)
@@ -312,9 +332,10 @@ export default class PromotionModuleService
} }
} }
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>( const existingPromotionsMap = new Map<
existingPromotions.map((promotion) => [promotion.code!, promotion]) string,
) InferEntityType<typeof Promotion>
>(existingPromotions.map((promotion) => [promotion.code!, promotion]))
for (let computedAction of computedActions) { for (let computedAction of computedActions) {
const promotion = existingPromotionsMap.get(computedAction.code) const promotion = existingPromotionsMap.get(computedAction.code)
@@ -433,27 +454,19 @@ export default class PromotionModuleService
const methodIdPromoValueMap = new Map<string, number>() const methodIdPromoValueMap = new Map<string, number>()
const automaticPromotions = preventAutoPromotions const promotionCodesToApply = [...promotionCodes, ...appliedCodes]
? []
: await this.listActivePromotions(
{ is_automatic: true },
{ select: ["code"] },
sharedContext
)
const automaticPromotionCodes = automaticPromotions.map((p) => p.code!)
const promotionCodesToApply = [
...promotionCodes,
...automaticPromotionCodes,
...appliedCodes,
]
const uniquePromotionCodes = Array.from(new Set(promotionCodesToApply)) const uniquePromotionCodes = Array.from(new Set(promotionCodesToApply))
const promotions = await this.listActivePromotions( const queryFilter = preventAutoPromotions
{ code: uniquePromotionCodes }, ? { code: uniquePromotionCodes }
: {
$or: [{ code: uniquePromotionCodes }, { is_automatic: true }],
}
const promotions = await this.listActivePromotions_(
queryFilter,
{ {
take: null,
order: { application_method: { value: "DESC" } }, order: { application_method: { value: "DESC" } },
relations: [ relations: [
"application_method", "application_method",
@@ -470,9 +483,18 @@ export default class PromotionModuleService
sharedContext sharedContext
) )
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>( const existingPromotionsMap = new Map<
promotions.map((promotion) => [promotion.code!, promotion]) string,
) InferEntityType<typeof Promotion>
>(promotions.map((promotion) => [promotion.code!, promotion]))
const automaticPromotionCodes: string[] = []
for (const promotion of promotions) {
if (promotion.is_automatic) {
automaticPromotionCodes.push(promotion.code!)
}
}
for (const [code, adjustments] of codeAdjustmentMap.entries()) { for (const [code, adjustments] of codeAdjustmentMap.entries()) {
for (const adjustment of adjustments.items) { for (const adjustment of adjustments.items) {
@@ -492,11 +514,14 @@ export default class PromotionModuleService
} }
} }
const promotionCodeSet = new Set<string>(promotionCodes)
const automaticPromotionCodeSet = new Set<string>(automaticPromotionCodes)
const sortedPromotionsToApply = promotions const sortedPromotionsToApply = promotions
.filter( .filter(
(p) => (p) =>
promotionCodes.includes(p.code!) || promotionCodeSet.has(p.code!) ||
automaticPromotionCodes.includes(p.code!) automaticPromotionCodeSet.has(p.code!)
) )
.sort(ComputeActionUtils.sortByBuyGetType) .sort(ComputeActionUtils.sortByBuyGetType)
@@ -511,15 +536,15 @@ export default class PromotionModuleService
for (const promotionToApply of sortedPromotionsToApply) { for (const promotionToApply of sortedPromotionsToApply) {
const promotion = existingPromotionsMap.get(promotionToApply.code!)! const promotion = existingPromotionsMap.get(promotionToApply.code!)!
if (!promotion.application_method) {
continue
}
const { const {
application_method: applicationMethod, application_method: applicationMethod,
rules: promotionRules = [], rules: promotionRules = [],
} = promotion } = promotion
if (!applicationMethod) {
continue
}
const isCurrencyCodeValid = const isCurrencyCodeValid =
!isPresent(applicationMethod.currency_code) || !isPresent(applicationMethod.currency_code) ||
applicationContext.currency_code === applicationMethod.currency_code applicationContext.currency_code === applicationMethod.currency_code
@@ -1040,15 +1065,24 @@ export default class PromotionModuleService
) { ) {
const promotionRuleIds = data.map((d) => d.id) const promotionRuleIds = data.map((d) => d.id)
const promotionRules = await this.listPromotionRules( const promotionRules = await this.promotionRuleService_.list(
{ id: promotionRuleIds }, { id: promotionRuleIds },
{ relations: ["values"] }, { relations: ["values"] },
sharedContext sharedContext
) )
const existingPromotionRuleIds: string[] = []
const promotionRulesMap: Map<string, PromotionTypes.PromotionRuleDTO> =
new Map()
for (const promotionRule of promotionRules) {
existingPromotionRuleIds.push(promotionRule.id)
promotionRulesMap.set(promotionRule.id, promotionRule)
}
const invalidRuleId = arrayDifference( const invalidRuleId = arrayDifference(
deduplicate(promotionRuleIds), deduplicate(promotionRuleIds),
promotionRules.map((pr) => pr.id) existingPromotionRuleIds
) )
if (invalidRuleId.length) { if (invalidRuleId.length) {
@@ -1058,10 +1092,6 @@ export default class PromotionModuleService
) )
} }
const promotionRulesMap = new Map<string, PromotionTypes.PromotionRuleDTO>(
promotionRules.map((pr) => [pr.id, pr])
)
const rulesToUpdate: PromotionTypes.UpdatePromotionRuleDTO[] = [] const rulesToUpdate: PromotionTypes.UpdatePromotionRuleDTO[] = []
const ruleValueIdsToDelete: string[] = [] const ruleValueIdsToDelete: string[] = []
const ruleValuesToCreate: CreatePromotionRuleValueDTO[] = [] const ruleValuesToCreate: CreatePromotionRuleValueDTO[] = []
@@ -1489,15 +1519,16 @@ export default class PromotionModuleService
const updateBudgetData: UpdateCampaignBudgetDTO[] = [] const updateBudgetData: UpdateCampaignBudgetDTO[] = []
const createBudgetData: CreateCampaignBudgetDTO[] = [] const createBudgetData: CreateCampaignBudgetDTO[] = []
const existingCampaigns = await this.listCampaigns( const existingCampaigns = await this.campaignService_.list(
{ id: campaignIds }, { id: campaignIds },
{ relations: ["budget"] }, { relations: ["budget"] },
sharedContext sharedContext
) )
const existingCampaignsMap = new Map<string, PromotionTypes.CampaignDTO>( const existingCampaignsMap = new Map<
existingCampaigns.map((campaign) => [campaign.id, campaign]) string,
) InferEntityType<typeof Campaign>
>(existingCampaigns.map((campaign) => [campaign.id, campaign]))
for (const updateCampaignData of data) { for (const updateCampaignData of data) {
const { budget: budgetData, ...campaignData } = updateCampaignData const { budget: budgetData, ...campaignData } = updateCampaignData
@@ -1,6 +1,7 @@
import { import {
BigNumberInput, BigNumberInput,
ComputeActionItemLine, ComputeActionItemLine,
InferEntityType,
PromotionTypes, PromotionTypes,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -12,6 +13,7 @@ import {
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations" import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage" import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
export type EligibleItem = { export type EligibleItem = {
item_id: string item_id: string
@@ -23,7 +25,7 @@ function sortByPrice(a: ComputeActionItemLine, b: ComputeActionItemLine) {
} }
function isValidPromotionContext( function isValidPromotionContext(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsContext: ComputeActionItemLine[] itemsContext: ComputeActionItemLine[]
): boolean { ): boolean {
if (!itemsContext) { if (!itemsContext) {
@@ -52,7 +54,7 @@ function isValidPromotionContext(
} }
function normalizePromotionApplicationConfiguration( function normalizePromotionApplicationConfiguration(
promotion: PromotionTypes.PromotionDTO promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>
) { ) {
const minimumBuyQuantity = MathBN.convert( const minimumBuyQuantity = MathBN.convert(
promotion.application_method?.buy_rules_min_quantity ?? 0 promotion.application_method?.buy_rules_min_quantity ?? 0
@@ -275,7 +277,7 @@ function applyPromotionToTargetItems(
targetItems: EligibleItem[], targetItems: EligibleItem[],
itemIdPromotionAmountMap: Map<string, BigNumberInput>, itemIdPromotionAmountMap: Map<string, BigNumberInput>,
methodIdPromoValueMap: Map<string, BigNumberInput>, methodIdPromoValueMap: Map<string, BigNumberInput>,
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsMap: Map<string, ComputeActionItemLine>, itemsMap: Map<string, ComputeActionItemLine>,
applicationConfig: PromotionConfig applicationConfig: PromotionConfig
): { ): {
@@ -456,7 +458,7 @@ function filterItemsByPromotionRules(
} }
export function getComputedActionsForBuyGet( export function getComputedActionsForBuyGet(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsContext: ComputeActionItemLine[], itemsContext: ComputeActionItemLine[],
methodIdPromoValueMap: Map<string, BigNumberInput>, methodIdPromoValueMap: Map<string, BigNumberInput>,
eligibleBuyItemMap: Map<string, EligibleItem[]>, eligibleBuyItemMap: Map<string, EligibleItem[]>,
@@ -1,4 +1,9 @@
import { ApplicationMethodAllocationValues, BigNumberInput, PromotionTypes, } from "@medusajs/framework/types" import {
ApplicationMethodAllocationValues,
BigNumberInput,
InferEntityType,
PromotionTypes,
} from "@medusajs/framework/types"
import { import {
ApplicationMethodAllocation, ApplicationMethodAllocation,
ApplicationMethodTargetType, ApplicationMethodTargetType,
@@ -10,6 +15,7 @@ import {
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations" import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage" import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
function validateContext( function validateContext(
contextKey: string, contextKey: string,
@@ -24,7 +30,7 @@ function validateContext(
} }
export function getComputedActionsForItems( export function getComputedActionsForItems(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
items: PromotionTypes.ComputeActionContext[TargetType.ITEMS], items: PromotionTypes.ComputeActionContext[TargetType.ITEMS],
appliedPromotionsMap: Map<string, number>, appliedPromotionsMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues allocationOverride?: ApplicationMethodAllocationValues
@@ -40,7 +46,7 @@ export function getComputedActionsForItems(
} }
function applyPromotionToItems( function applyPromotionToItems(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
items: PromotionTypes.ComputeActionContext[TargetType.ITEMS], items: PromotionTypes.ComputeActionContext[TargetType.ITEMS],
appliedPromotionsMap: Map<string, BigNumberInput>, appliedPromotionsMap: Map<string, BigNumberInput>,
allocationOverride?: ApplicationMethodAllocationValues allocationOverride?: ApplicationMethodAllocationValues
@@ -149,7 +155,7 @@ function getValidItemsForPromotion(
items: items:
| PromotionTypes.ComputeActionContext[TargetType.ITEMS] | PromotionTypes.ComputeActionContext[TargetType.ITEMS]
| PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS], | PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
promotion: PromotionTypes.PromotionDTO promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>
) { ) {
if (!items?.length || !promotion?.application_method) { if (!items?.length || !promotion?.application_method) {
return [] return []
@@ -1,4 +1,8 @@
import { BigNumberInput, PromotionTypes } from "@medusajs/framework/types" import {
BigNumberInput,
InferEntityType,
PromotionTypes,
} from "@medusajs/framework/types"
import { import {
ApplicationMethodAllocation, ApplicationMethodAllocation,
ApplicationMethodTargetType, ApplicationMethodTargetType,
@@ -9,9 +13,10 @@ import {
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations" import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage" import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
export function getComputedActionsForShippingMethods( export function getComputedActionsForShippingMethods(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
shippingMethodApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS], shippingMethodApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, number> methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] { ): PromotionTypes.ComputeActions[] {
@@ -47,7 +52,7 @@ export function getComputedActionsForShippingMethods(
} }
export function applyPromotionToShippingMethods( export function applyPromotionToShippingMethods(
promotion: PromotionTypes.PromotionDTO, promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
shippingMethods: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS], shippingMethods: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, BigNumberInput> methodIdPromoValueMap: Map<string, BigNumberInput>
): PromotionTypes.ComputeActions[] { ): PromotionTypes.ComputeActions[] {
@@ -126,10 +131,16 @@ export function applyPromotionToShippingMethods(
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0 const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
const applicableTotal = MathBN.sub(method.subtotal, appliedPromoValue) const applicableTotal = MathBN.sub(method.subtotal, appliedPromoValue)
let applicablePromotionValue = MathBN.mult(MathBN.div(applicableTotal, totalApplicableValue), promotionValue) let applicablePromotionValue = MathBN.mult(
MathBN.div(applicableTotal, totalApplicableValue),
promotionValue
)
if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) {
applicablePromotionValue = MathBN.mult(MathBN.div(promotionValue, 100), applicableTotal) applicablePromotionValue = MathBN.mult(
MathBN.div(promotionValue, 100),
applicableTotal
)
} }
const amount = MathBN.min(applicablePromotionValue, applicableTotal) const amount = MathBN.min(applicablePromotionValue, applicableTotal)
@@ -1,6 +1,7 @@
import { import {
BigNumberInput, BigNumberInput,
CampaignBudgetExceededAction, CampaignBudgetExceededAction,
InferEntityType,
PromotionDTO, PromotionDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
@@ -8,9 +9,10 @@ import {
ComputedActions, ComputedActions,
MathBN, MathBN,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { Promotion } from "@models"
export function computeActionForBudgetExceeded( export function computeActionForBudgetExceeded(
promotion: PromotionDTO, promotion: PromotionDTO | InferEntityType<typeof Promotion>,
amount: BigNumberInput amount: BigNumberInput
): CampaignBudgetExceededAction | void { ): CampaignBudgetExceededAction | void {
const campaignBudget = promotion.campaign?.budget const campaignBudget = promotion.campaign?.budget
@@ -1,5 +1,6 @@
import { import {
ApplicationMethodTargetTypeValues, ApplicationMethodTargetTypeValues,
InferEntityType,
PromotionRuleDTO, PromotionRuleDTO,
PromotionRuleOperatorValues, PromotionRuleOperatorValues,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
@@ -12,6 +13,7 @@ import {
isString, isString,
pickValueFromObject, pickValueFromObject,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
import { PromotionRule } from "@models"
import { CreatePromotionRuleDTO } from "@types" import { CreatePromotionRuleDTO } from "@types"
export function validatePromotionRuleAttributes( export function validatePromotionRuleAttributes(
@@ -51,7 +53,7 @@ export function validatePromotionRuleAttributes(
} }
export function areRulesValidForContext( export function areRulesValidForContext(
rules: PromotionRuleDTO[], rules: PromotionRuleDTO[] | InferEntityType<typeof PromotionRule>[],
context: Record<string, any>, context: Record<string, any>,
contextScope: ApplicationMethodTargetTypeValues contextScope: ApplicationMethodTargetTypeValues
): boolean { ): boolean {
@@ -63,14 +65,17 @@ export function areRulesValidForContext(
const isShippingScope = const isShippingScope =
contextScope === ApplicationMethodTargetType.SHIPPING_METHODS contextScope === ApplicationMethodTargetType.SHIPPING_METHODS
return rules.every((rule) => { return ("initialized" in rules ? [...rules] : rules).every((rule) => {
if (!rule.attribute || !rule.values?.length) { if (!rule.attribute || !rule.values?.length) {
return false return false
} }
const validRuleValues = rule.values const validRuleValues: string[] = []
.filter((v) => isString(v.value)) for (const value of rule.values) {
.map((v) => v.value as string) if (isString(value.value)) {
validRuleValues.push(value.value as string)
}
}
if (!validRuleValues.length) { if (!validRuleValues.length) {
return false return false
@@ -539,7 +539,7 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
throwOnError: false, throwOnError: false,
}) })
await setTimeout(1000) await setTimeout(4000)
expect(lastExepectHaveBeenCalledTimes).toEqual(1) expect(lastExepectHaveBeenCalledTimes).toEqual(1)
}) })