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
2025-09-10 12:20:31 +02:00
committed by GitHub
parent d978749603
commit ac09b3cbef
9 changed files with 129 additions and 67 deletions
+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",
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
"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: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",
@@ -139,11 +139,26 @@ export default class PromotionModuleService
}
@InjectManager()
listActivePromotions(
async listActivePromotions(
filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>,
sharedContext?: Context
): 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
const now = new Date()
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()
@@ -185,7 +204,7 @@ export default class PromotionModuleService
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const promotionCodeUsageMap = new Map<string, boolean>()
const existingPromotions = await this.listActivePromotions(
const existingPromotions = await this.listActivePromotions_(
{ code: promotionCodes },
{ relations: ["campaign", "campaign.budget"] },
sharedContext
@@ -200,9 +219,10 @@ export default class PromotionModuleService
}
}
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
existingPromotions.map((promotion) => [promotion.code!, promotion])
)
const existingPromotionsMap = new Map<
string,
InferEntityType<typeof Promotion>
>(existingPromotions.map((promotion) => [promotion.code!, promotion]))
for (let computedAction of computedActions) {
const promotion = existingPromotionsMap.get(computedAction.code)
@@ -293,7 +313,7 @@ export default class PromotionModuleService
const promotionCodeUsageMap = new Map<string, boolean>()
const campaignBudgetMap = new Map<string, UpdateCampaignBudgetDTO>()
const existingPromotions = await this.listActivePromotions(
const existingPromotions = await this.listActivePromotions_(
{
code: computedActions
.map((computedAction) => computedAction.code)
@@ -312,9 +332,10 @@ export default class PromotionModuleService
}
}
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
existingPromotions.map((promotion) => [promotion.code!, promotion])
)
const existingPromotionsMap = new Map<
string,
InferEntityType<typeof Promotion>
>(existingPromotions.map((promotion) => [promotion.code!, promotion]))
for (let computedAction of computedActions) {
const promotion = existingPromotionsMap.get(computedAction.code)
@@ -433,27 +454,19 @@ export default class PromotionModuleService
const methodIdPromoValueMap = new Map<string, number>()
const automaticPromotions = preventAutoPromotions
? []
: await this.listActivePromotions(
{ is_automatic: true },
{ select: ["code"] },
sharedContext
)
const automaticPromotionCodes = automaticPromotions.map((p) => p.code!)
const promotionCodesToApply = [
...promotionCodes,
...automaticPromotionCodes,
...appliedCodes,
]
const promotionCodesToApply = [...promotionCodes, ...appliedCodes]
const uniquePromotionCodes = Array.from(new Set(promotionCodesToApply))
const promotions = await this.listActivePromotions(
{ code: uniquePromotionCodes },
const queryFilter = preventAutoPromotions
? { code: uniquePromotionCodes }
: {
$or: [{ code: uniquePromotionCodes }, { is_automatic: true }],
}
const promotions = await this.listActivePromotions_(
queryFilter,
{
take: null,
order: { application_method: { value: "DESC" } },
relations: [
"application_method",
@@ -470,9 +483,18 @@ export default class PromotionModuleService
sharedContext
)
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
promotions.map((promotion) => [promotion.code!, promotion])
)
const existingPromotionsMap = new Map<
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 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
.filter(
(p) =>
promotionCodes.includes(p.code!) ||
automaticPromotionCodes.includes(p.code!)
promotionCodeSet.has(p.code!) ||
automaticPromotionCodeSet.has(p.code!)
)
.sort(ComputeActionUtils.sortByBuyGetType)
@@ -511,15 +536,15 @@ export default class PromotionModuleService
for (const promotionToApply of sortedPromotionsToApply) {
const promotion = existingPromotionsMap.get(promotionToApply.code!)!
if (!promotion.application_method) {
continue
}
const {
application_method: applicationMethod,
rules: promotionRules = [],
} = promotion
if (!applicationMethod) {
continue
}
const isCurrencyCodeValid =
!isPresent(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 promotionRules = await this.listPromotionRules(
const promotionRules = await this.promotionRuleService_.list(
{ id: promotionRuleIds },
{ relations: ["values"] },
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(
deduplicate(promotionRuleIds),
promotionRules.map((pr) => pr.id)
existingPromotionRuleIds
)
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 ruleValueIdsToDelete: string[] = []
const ruleValuesToCreate: CreatePromotionRuleValueDTO[] = []
@@ -1489,15 +1519,16 @@ export default class PromotionModuleService
const updateBudgetData: UpdateCampaignBudgetDTO[] = []
const createBudgetData: CreateCampaignBudgetDTO[] = []
const existingCampaigns = await this.listCampaigns(
const existingCampaigns = await this.campaignService_.list(
{ id: campaignIds },
{ relations: ["budget"] },
sharedContext
)
const existingCampaignsMap = new Map<string, PromotionTypes.CampaignDTO>(
existingCampaigns.map((campaign) => [campaign.id, campaign])
)
const existingCampaignsMap = new Map<
string,
InferEntityType<typeof Campaign>
>(existingCampaigns.map((campaign) => [campaign.id, campaign]))
for (const updateCampaignData of data) {
const { budget: budgetData, ...campaignData } = updateCampaignData
@@ -1,6 +1,7 @@
import {
BigNumberInput,
ComputeActionItemLine,
InferEntityType,
PromotionTypes,
} from "@medusajs/framework/types"
import {
@@ -12,6 +13,7 @@ import {
} from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
export type EligibleItem = {
item_id: string
@@ -23,7 +25,7 @@ function sortByPrice(a: ComputeActionItemLine, b: ComputeActionItemLine) {
}
function isValidPromotionContext(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsContext: ComputeActionItemLine[]
): boolean {
if (!itemsContext) {
@@ -52,7 +54,7 @@ function isValidPromotionContext(
}
function normalizePromotionApplicationConfiguration(
promotion: PromotionTypes.PromotionDTO
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>
) {
const minimumBuyQuantity = MathBN.convert(
promotion.application_method?.buy_rules_min_quantity ?? 0
@@ -275,7 +277,7 @@ function applyPromotionToTargetItems(
targetItems: EligibleItem[],
itemIdPromotionAmountMap: Map<string, BigNumberInput>,
methodIdPromoValueMap: Map<string, BigNumberInput>,
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsMap: Map<string, ComputeActionItemLine>,
applicationConfig: PromotionConfig
): {
@@ -456,7 +458,7 @@ function filterItemsByPromotionRules(
}
export function getComputedActionsForBuyGet(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
itemsContext: ComputeActionItemLine[],
methodIdPromoValueMap: Map<string, BigNumberInput>,
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 {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
@@ -10,6 +15,7 @@ import {
} from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
function validateContext(
contextKey: string,
@@ -24,7 +30,7 @@ function validateContext(
}
export function getComputedActionsForItems(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
items: PromotionTypes.ComputeActionContext[TargetType.ITEMS],
appliedPromotionsMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues
@@ -40,7 +46,7 @@ export function getComputedActionsForItems(
}
function applyPromotionToItems(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
items: PromotionTypes.ComputeActionContext[TargetType.ITEMS],
appliedPromotionsMap: Map<string, BigNumberInput>,
allocationOverride?: ApplicationMethodAllocationValues
@@ -149,7 +155,7 @@ function getValidItemsForPromotion(
items:
| PromotionTypes.ComputeActionContext[TargetType.ITEMS]
| PromotionTypes.ComputeActionContext[TargetType.SHIPPING_METHODS],
promotion: PromotionTypes.PromotionDTO
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>
) {
if (!items?.length || !promotion?.application_method) {
return []
@@ -1,4 +1,8 @@
import { BigNumberInput, PromotionTypes } from "@medusajs/framework/types"
import {
BigNumberInput,
InferEntityType,
PromotionTypes,
} from "@medusajs/framework/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
@@ -9,9 +13,10 @@ import {
} from "@medusajs/framework/utils"
import { areRulesValidForContext } from "../validations"
import { computeActionForBudgetExceeded } from "./usage"
import { Promotion } from "@models"
export function getComputedActionsForShippingMethods(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
shippingMethodApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] {
@@ -47,7 +52,7 @@ export function getComputedActionsForShippingMethods(
}
export function applyPromotionToShippingMethods(
promotion: PromotionTypes.PromotionDTO,
promotion: PromotionTypes.PromotionDTO | InferEntityType<typeof Promotion>,
shippingMethods: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, BigNumberInput>
): PromotionTypes.ComputeActions[] {
@@ -126,10 +131,16 @@ export function applyPromotionToShippingMethods(
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
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) {
applicablePromotionValue = MathBN.mult(MathBN.div(promotionValue, 100), applicableTotal)
applicablePromotionValue = MathBN.mult(
MathBN.div(promotionValue, 100),
applicableTotal
)
}
const amount = MathBN.min(applicablePromotionValue, applicableTotal)
@@ -1,6 +1,7 @@
import {
BigNumberInput,
CampaignBudgetExceededAction,
InferEntityType,
PromotionDTO,
} from "@medusajs/framework/types"
import {
@@ -8,9 +9,10 @@ import {
ComputedActions,
MathBN,
} from "@medusajs/framework/utils"
import { Promotion } from "@models"
export function computeActionForBudgetExceeded(
promotion: PromotionDTO,
promotion: PromotionDTO | InferEntityType<typeof Promotion>,
amount: BigNumberInput
): CampaignBudgetExceededAction | void {
const campaignBudget = promotion.campaign?.budget
@@ -1,5 +1,6 @@
import {
ApplicationMethodTargetTypeValues,
InferEntityType,
PromotionRuleDTO,
PromotionRuleOperatorValues,
} from "@medusajs/framework/types"
@@ -12,6 +13,7 @@ import {
isString,
pickValueFromObject,
} from "@medusajs/framework/utils"
import { PromotionRule } from "@models"
import { CreatePromotionRuleDTO } from "@types"
export function validatePromotionRuleAttributes(
@@ -51,7 +53,7 @@ export function validatePromotionRuleAttributes(
}
export function areRulesValidForContext(
rules: PromotionRuleDTO[],
rules: PromotionRuleDTO[] | InferEntityType<typeof PromotionRule>[],
context: Record<string, any>,
contextScope: ApplicationMethodTargetTypeValues
): boolean {
@@ -63,14 +65,17 @@ export function areRulesValidForContext(
const isShippingScope =
contextScope === ApplicationMethodTargetType.SHIPPING_METHODS
return rules.every((rule) => {
return ("initialized" in rules ? [...rules] : rules).every((rule) => {
if (!rule.attribute || !rule.values?.length) {
return false
}
const validRuleValues = rule.values
.filter((v) => isString(v.value))
.map((v) => v.value as string)
const validRuleValues: string[] = []
for (const value of rule.values) {
if (isString(value.value)) {
validRuleValues.push(value.value as string)
}
}
if (!validRuleValues.length) {
return false
@@ -539,7 +539,7 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
throwOnError: false,
})
await setTimeout(1000)
await setTimeout(4000)
expect(lastExepectHaveBeenCalledTimes).toEqual(1)
})