feat(workflows-sdk,core-flows,medusa,types): add workflow to update promotions to cart (#6474)
what: - adds API + workflow to add/remove promotions in a cart - minor fixes in promotions module - minor type fixes in cart module - typing fix in workflows-sdk (Thanks @adrien2p) - fix step result in workflows-sdk (Thanks @adrien2p) RESOLVES CORE-1768 Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
1 parent
63be07031b
commit
ac86362e81
30 files changed
+1187
-101
No files matched your search
@@ -11,11 +11,11 @@ import {
|
||||
CampaignBudgetType,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isString,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
PromotionType,
|
||||
isString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
ApplicationMethod,
|
||||
@@ -38,9 +38,9 @@ import {
|
||||
UpdatePromotionDTO,
|
||||
} from "@types"
|
||||
import {
|
||||
ComputeActionUtils,
|
||||
allowedAllocationForQuantity,
|
||||
areRulesValidForContext,
|
||||
ComputeActionUtils,
|
||||
validateApplicationMethodAttributes,
|
||||
validatePromotionRuleAttributes,
|
||||
} from "@utils"
|
||||
@@ -166,10 +166,10 @@ export default class PromotionModuleService<
|
||||
if (campaignBudget.type === CampaignBudgetType.SPEND) {
|
||||
const campaignBudgetData = promotionCodeCampaignBudgetMap.get(
|
||||
campaignBudget.id
|
||||
) || { id: campaignBudget.id, used: campaignBudget.used || 0 }
|
||||
) || { id: campaignBudget.id, used: campaignBudget.used ?? 0 }
|
||||
|
||||
campaignBudgetData.used =
|
||||
(campaignBudgetData.used || 0) + computedAction.amount
|
||||
(campaignBudgetData.used ?? 0) + computedAction.amount
|
||||
|
||||
if (
|
||||
campaignBudget.limit &&
|
||||
@@ -194,7 +194,7 @@ export default class PromotionModuleService<
|
||||
|
||||
const campaignBudgetData = {
|
||||
id: campaignBudget.id,
|
||||
used: (campaignBudget.used || 0) + 1,
|
||||
used: (campaignBudget.used ?? 0) + 1,
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -225,10 +225,12 @@ export default class PromotionModuleService<
|
||||
}
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async computeActions(
|
||||
promotionCodes: string[],
|
||||
applicationContext: PromotionTypes.ComputeActionContext,
|
||||
options: PromotionTypes.ComputeActionOptions = {}
|
||||
options: PromotionTypes.ComputeActionOptions = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<PromotionTypes.ComputeActions[]> {
|
||||
const { prevent_auto_promotions: preventAutoPromotions } = options
|
||||
const computedActions: PromotionTypes.ComputeActions[] = []
|
||||
@@ -238,12 +240,16 @@ export default class PromotionModuleService<
|
||||
const appliedShippingCodes: string[] = []
|
||||
const codeAdjustmentMap = new Map<
|
||||
string,
|
||||
PromotionTypes.ComputeActionAdjustmentLine
|
||||
PromotionTypes.ComputeActionAdjustmentLine[]
|
||||
>()
|
||||
const methodIdPromoValueMap = new Map<string, number>()
|
||||
const automaticPromotions = preventAutoPromotions
|
||||
? []
|
||||
: await this.list({ is_automatic: true }, { select: ["code"] })
|
||||
: await this.list(
|
||||
{ is_automatic: true },
|
||||
{ select: ["code"] },
|
||||
sharedContext
|
||||
)
|
||||
|
||||
const automaticPromotionCodes = automaticPromotions.map((p) => p.code!)
|
||||
const promotionCodesToApply = [
|
||||
@@ -254,7 +260,11 @@ export default class PromotionModuleService<
|
||||
items.forEach((item) => {
|
||||
item.adjustments?.forEach((adjustment) => {
|
||||
if (isString(adjustment.code)) {
|
||||
codeAdjustmentMap.set(adjustment.code, adjustment)
|
||||
const adjustments = codeAdjustmentMap.get(adjustment.code) || []
|
||||
|
||||
adjustments.push(adjustment)
|
||||
|
||||
codeAdjustmentMap.set(adjustment.code, adjustments)
|
||||
appliedItemCodes.push(adjustment.code)
|
||||
}
|
||||
})
|
||||
@@ -263,7 +273,11 @@ export default class PromotionModuleService<
|
||||
shippingMethods.forEach((shippingMethod) => {
|
||||
shippingMethod.adjustments?.forEach((adjustment) => {
|
||||
if (isString(adjustment.code)) {
|
||||
codeAdjustmentMap.set(adjustment.code, adjustment)
|
||||
const adjustments = codeAdjustmentMap.get(adjustment.code) || []
|
||||
|
||||
adjustments.push(adjustment)
|
||||
|
||||
codeAdjustmentMap.set(adjustment.code, adjustments)
|
||||
appliedShippingCodes.push(adjustment.code)
|
||||
}
|
||||
})
|
||||
@@ -292,6 +306,7 @@ export default class PromotionModuleService<
|
||||
}
|
||||
)
|
||||
|
||||
const appliedCodes = [...appliedShippingCodes, ...appliedItemCodes]
|
||||
const sortedPermissionsToApply = promotions
|
||||
.filter((p) => promotionCodesToApply.includes(p.code!))
|
||||
.sort(ComputeActionUtils.sortByBuyGetType)
|
||||
@@ -300,7 +315,7 @@ export default class PromotionModuleService<
|
||||
promotions.map((promotion) => [promotion.code!, promotion])
|
||||
)
|
||||
|
||||
for (const appliedCode of [...appliedShippingCodes, ...appliedItemCodes]) {
|
||||
for (const appliedCode of appliedCodes) {
|
||||
const promotion = existingPromotionsMap.get(appliedCode)
|
||||
|
||||
if (!promotion) {
|
||||
@@ -310,24 +325,28 @@ export default class PromotionModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
if (promotionCodes.includes(appliedCode)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (appliedItemCodes.includes(appliedCode)) {
|
||||
computedActions.push({
|
||||
action: "removeItemAdjustment",
|
||||
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
||||
code: appliedCode,
|
||||
})
|
||||
const adjustments = codeAdjustmentMap.get(appliedCode) || []
|
||||
|
||||
adjustments.forEach((adjustment) =>
|
||||
computedActions.push({
|
||||
action: "removeItemAdjustment",
|
||||
adjustment_id: adjustment.id,
|
||||
code: appliedCode,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (appliedShippingCodes.includes(appliedCode)) {
|
||||
computedActions.push({
|
||||
action: "removeShippingMethodAdjustment",
|
||||
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
|
||||
code: appliedCode,
|
||||
})
|
||||
const adjustments = codeAdjustmentMap.get(appliedCode) || []
|
||||
|
||||
adjustments.forEach((adjustment) =>
|
||||
computedActions.push({
|
||||
action: "removeShippingMethodAdjustment",
|
||||
adjustment_id: adjustment.id,
|
||||
code: appliedCode,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
ComputedActions,
|
||||
MedusaError,
|
||||
PromotionType,
|
||||
isPresent,
|
||||
} from "@medusajs/utils"
|
||||
import { areRulesValidForContext } from "../validations"
|
||||
import { computeActionForBudgetExceeded } from "./usage"
|
||||
|
||||
// TODO: calculations should eventually move to a totals util outside of the module
|
||||
export function getComputedActionsForBuyGet(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
|
||||
@@ -45,6 +47,7 @@ export function getComputedActionsForBuyGet(
|
||||
|
||||
const validItemsForTargetRules = itemsContext
|
||||
.filter((item) => areRulesValidForContext(targetRules, item))
|
||||
.filter((item) => isPresent(item.subtotal) && isPresent(item.quantity))
|
||||
.sort((a, b) => {
|
||||
const aPrice = a.subtotal / a.quantity
|
||||
const bPrice = b.subtotal / b.quantity
|
||||
@@ -55,7 +58,7 @@ export function getComputedActionsForBuyGet(
|
||||
let remainingQtyToApply = applyToQuantity
|
||||
|
||||
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 amount = (method.subtotal / method.quantity) * multiplier
|
||||
const newRemainingQtyToApply = remainingQtyToApply - multiplier
|
||||
|
||||
@@ -49,6 +49,7 @@ export function getComputedActionsForItems(
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: calculations should eventually move to a totals util outside of the module
|
||||
export function applyPromotionToItems(
|
||||
promotion: PromotionTypes.PromotionDTO,
|
||||
items: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
|
||||
@@ -63,7 +64,11 @@ export function applyPromotionToItems(
|
||||
[allocation, allocationOverride].includes(ApplicationMethodAllocation.EACH)
|
||||
) {
|
||||
for (const method of items!) {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||
if (!method.subtotal || !method.quantity) {
|
||||
continue
|
||||
}
|
||||
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
const quantityMultiplier = Math.min(
|
||||
method.quantity,
|
||||
applicationMethod?.max_quantity!
|
||||
@@ -111,16 +116,20 @@ export function applyPromotionToItems(
|
||||
)
|
||||
) {
|
||||
const totalApplicableValue = items!.reduce((acc, method) => {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||
return (
|
||||
acc +
|
||||
(method.subtotal / method.quantity) * method.quantity -
|
||||
appliedPromoValue
|
||||
)
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
const perItemCost = method.subtotal
|
||||
? method.subtotal / method.quantity
|
||||
: 0
|
||||
|
||||
return acc + perItemCost * method.quantity - appliedPromoValue
|
||||
}, 0)
|
||||
|
||||
for (const method of items!) {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||
if (!method.subtotal || !method.quantity) {
|
||||
continue
|
||||
}
|
||||
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
||||
const applicableTotal =
|
||||
(method.subtotal / method.quantity) * method.quantity -
|
||||
|
||||
@@ -55,7 +55,11 @@ export function applyPromotionToShippingMethods(
|
||||
|
||||
if (allocation === ApplicationMethodAllocation.EACH) {
|
||||
for (const method of shippingMethods!) {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||
if (!method.subtotal) {
|
||||
continue
|
||||
}
|
||||
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
let promotionValue = parseFloat(applicationMethod!.value!)
|
||||
const applicableTotal = method.subtotal - appliedPromoValue
|
||||
|
||||
@@ -93,9 +97,9 @@ export function applyPromotionToShippingMethods(
|
||||
|
||||
if (allocation === ApplicationMethodAllocation.ACROSS) {
|
||||
const totalApplicableValue = shippingMethods!.reduce((acc, method) => {
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
|
||||
const appliedPromoValue = methodIdPromoValueMap.get(method.id) ?? 0
|
||||
|
||||
return acc + method.subtotal - appliedPromoValue
|
||||
return acc + (method.subtotal ?? 0) - appliedPromoValue
|
||||
}, 0)
|
||||
|
||||
if (totalApplicableValue <= 0) {
|
||||
@@ -103,9 +107,13 @@ export function applyPromotionToShippingMethods(
|
||||
}
|
||||
|
||||
for (const method of shippingMethods!) {
|
||||
if (!method.subtotal) {
|
||||
continue
|
||||
}
|
||||
|
||||
const promotionValue = parseFloat(applicationMethod!.value!)
|
||||
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?
|
||||
let applicablePromotionValue =
|
||||
|
||||
@@ -24,7 +24,7 @@ export function computeActionForBudgetExceeded(
|
||||
return
|
||||
}
|
||||
|
||||
const campaignBudgetUsed = campaignBudget.used || 0
|
||||
const campaignBudgetUsed = campaignBudget.used ?? 0
|
||||
const totalUsed =
|
||||
campaignBudget.type === CampaignBudgetType.SPEND
|
||||
? campaignBudgetUsed + amount
|
||||
|
||||
Reference in new issue
Block a user