feat(medusa,core-flows,types): fix bugs with cart promotions (#7438)

what:

The true source of promotions applied are present in the adjustments, previously we had links as the source of truth, but some issues popped up when you're trying to delete a line item as it was holding onto stale promotion data.

For this to truly work properly in the current form, we would have to pass data from the deleted line item step to the refresh promotion step and mutate the links twice to get the correct promotions for a cart. Additionally, the final list of codes to apply, comes from computeAction, because some previously applied codes could no longer be valid. The combination of passing codes from adjustments to computeActions and the codes returned from the computedActions are how we can accurately calculate the right promotions to apply. 

With this change, the promotions step always look at the latest line item adjustments of the cart, pass them to compute actions, pick the correct codes to apply from the compute actions and then mutate the links based on the final result. This way, we perform the correct calculations for a cart and have promotion links that are correct.
This commit is contained in:
Riqwan Thamir
2024-05-24 11:48:14 +02:00
committed by GitHub
parent d29d5436fb
commit ff870482bb
10 changed files with 122 additions and 49 deletions

View File

@@ -1,9 +1,10 @@
import { LinkModuleUtils, ModuleRegistrationName } from "@medusajs/modules-sdk"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { CartDTO, IPromotionModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput {
cart: CartDTO
promotionCodesToApply: string[]
}
export const getActionsToComputeFromPromotionsStepId =
@@ -11,26 +12,13 @@ export const getActionsToComputeFromPromotionsStepId =
export const getActionsToComputeFromPromotionsStep = createStep(
getActionsToComputeFromPromotionsStepId,
async (data: StepInput, { container }) => {
const { cart } = data
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const { cart, promotionCodesToApply = [] } = data
const promotionService = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
const existingCartPromotionLinks = await remoteQuery({
cart_promotion: {
__args: { cart_id: [cart.id] },
fields: ["id", "cart_id", "promotion_id", "deleted_at"],
},
})
const existingPromotions = await promotionService.list(
{ id: existingCartPromotionLinks.map((l) => l.promotion_id) },
{ take: null, select: ["code"] }
)
const actionsToCompute = await promotionService.computeActions(
existingPromotions.map((p) => p.code!),
promotionCodesToApply,
cart as any
)

View File

@@ -0,0 +1,63 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPromotionModuleService } from "@medusajs/types"
import { PromotionActions } from "@medusajs/utils"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput {
cart: {
items?: { adjustments?: { code?: string }[] }[]
shipping_methods?: { adjustments?: { code?: string }[] }[]
}
promo_codes?: string[]
action?:
| PromotionActions.ADD
| PromotionActions.REMOVE
| PromotionActions.REPLACE
}
export const getPromotionCodesToApplyId = "get-promotion-codes-to-apply"
export const getPromotionCodesToApply = createStep(
getPromotionCodesToApplyId,
async (data: StepInput, { container }) => {
const { promo_codes = [], cart, action = PromotionActions.ADD } = data
const { items = [], shipping_methods = [] } = cart
const adjustmentCodes: string[] = []
const promotionService = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION
)
const objects = items.concat(shipping_methods)
objects.forEach((object) => {
object.adjustments?.forEach((adjustment) => {
if (adjustment.code && !adjustmentCodes.includes(adjustment.code)) {
adjustmentCodes.push(adjustment.code)
}
})
})
const promotionCodesToApply: Set<string> = new Set(
(
await promotionService.list(
{ code: adjustmentCodes },
{ select: ["code"], take: null }
)
).map((p) => p.code!)
)
if (action === PromotionActions.ADD) {
promo_codes.forEach((code) => promotionCodesToApply.add(code))
}
if (action === PromotionActions.REMOVE) {
promo_codes.forEach((code) => promotionCodesToApply.delete(code))
}
if (action === PromotionActions.REPLACE) {
promotionCodesToApply.clear()
promo_codes.forEach((code) => promotionCodesToApply.add(code))
}
return new StepResponse(Array.from(promotionCodesToApply))
}
)

View File

@@ -10,6 +10,7 @@ export * from "./find-or-create-customer"
export * from "./find-sales-channel"
export * from "./get-actions-to-compute-from-promotions"
export * from "./get-item-tax-lines"
export * from "./get-promotion-codes-to-apply"
export * from "./get-variant-price-sets"
export * from "./get-variants"
export * from "./prepare-adjustments-from-promotion-actions"

View File

@@ -65,11 +65,17 @@ export const prepareAdjustmentsFromPromotionActionsStep = createStep(
)
.map((a) => (a as RemoveShippingMethodAdjustment).adjustment_id)
const computedPromotionCodes = [
...lineItemAdjustmentsToCreate,
...shippingMethodAdjustmentsToCreate,
].map((adjustment) => adjustment.code)
return new StepResponse({
lineItemAdjustmentsToCreate,
lineItemAdjustmentIdsToRemove,
shippingMethodAdjustmentsToCreate,
shippingMethodAdjustmentIdsToRemove,
computedPromotionCodes,
})
}
)

View File

@@ -21,7 +21,6 @@ export const updateCartPromotionsStep = createStep(
updateCartPromotionsStepId,
async (data: StepInput, { container }) => {
const { promo_codes = [], id, action = PromotionActions.ADD } = data
const remoteLink = container.resolve(LinkModuleUtils.REMOTE_LINK)
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const promotionService = container.resolve<IPromotionModuleService>(
@@ -75,19 +74,14 @@ export const updateCartPromotionsStep = createStep(
}
}
const linksToDismissPromise = linksToDismiss.length
? remoteLink.dismiss(linksToDismiss)
: []
if (linksToDismiss.length) {
await remoteLink.dismiss(linksToDismiss)
}
const linksToCreatePromise = linksToCreate.length
? remoteLink.create(linksToCreate)
const createdLinks = linksToCreate.length
? await remoteLink.create(linksToCreate)
: []
const [_, createdLinks] = await Promise.all([
linksToDismissPromise,
linksToCreatePromise,
])
return new StepResponse(null, {
createdLinkIds: createdLinks.map((link) => link.id),
dismissedLinks: linksToDismiss,

View File

@@ -1,12 +1,21 @@
export const cartFieldsForRefreshSteps = [
"subtotal",
"item_subtotal",
"shipping_subtotal",
"region_id",
"currency_code",
"region.*",
"items.*",
"items.product.id",
"items.product.collection_id",
"items.product.categories.id",
"items.product.tags.id",
"items.adjustments.*",
"items.tax_lines.*",
"shipping_address.*",
"shipping_methods.*",
"shipping_methods.tax_lines*",
"shipping_methods.adjustments.*",
"shipping_methods.tax_lines.*",
"customer.*",
"customer.groups.*",
]

View File

@@ -4,16 +4,18 @@ import {
createWorkflow,
parallelize,
} from "@medusajs/workflows-sdk"
import { useRemoteQueryStep } from "../../../common"
import {
createLineItemAdjustmentsStep,
createShippingMethodAdjustmentsStep,
getActionsToComputeFromPromotionsStep,
getPromotionCodesToApply,
prepareAdjustmentsFromPromotionActionsStep,
removeLineItemAdjustmentsStep,
removeShippingMethodAdjustmentsStep,
retrieveCartStep,
updateCartPromotionsStep,
} from "../steps"
import { updateCartPromotionsStep } from "../steps/update-cart-promotions"
import { cartFieldsForRefreshSteps } from "../utils/fields"
type WorkflowInput = {
promoCodes: string[]
@@ -28,27 +30,22 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions"
export const updateCartPromotionsWorkflow = createWorkflow(
updateCartPromotionsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
const retrieveCartInput = {
id: input.cartId,
config: {
relations: [
"items",
"items.adjustments",
"shipping_methods",
"shipping_methods.adjustments",
],
},
}
const cart = useRemoteQueryStep({
entry_point: "cart",
fields: cartFieldsForRefreshSteps,
variables: { id: input.cartId },
list: false,
})
updateCartPromotionsStep({
id: input.cartId,
const promotionCodesToApply = getPromotionCodesToApply({
cart: cart,
promo_codes: input.promoCodes,
action: input.action || PromotionActions.ADD,
})
const cart = retrieveCartStep(retrieveCartInput)
const actions = getActionsToComputeFromPromotionsStep({
cart,
promotionCodesToApply,
})
const {
@@ -56,6 +53,7 @@ export const updateCartPromotionsWorkflow = createWorkflow(
lineItemAdjustmentIdsToRemove,
shippingMethodAdjustmentsToCreate,
shippingMethodAdjustmentIdsToRemove,
computedPromotionCodes,
} = prepareAdjustmentsFromPromotionActionsStep({ actions })
parallelize(
@@ -69,5 +67,11 @@ export const updateCartPromotionsWorkflow = createWorkflow(
createLineItemAdjustmentsStep({ lineItemAdjustmentsToCreate }),
createShippingMethodAdjustmentsStep({ shippingMethodAdjustmentsToCreate })
)
updateCartPromotionsStep({
id: input.cartId,
promo_codes: computedPromotionCodes,
action: PromotionActions.REPLACE,
})
}
)

View File

@@ -1,7 +1,8 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { refreshCartPromotionsStep } from "../../cart/steps/refresh-cart-promotions"
import { deleteLineItemsStep } from "../steps/delete-line-items"
type WorkflowInput = { ids: string[] }
type WorkflowInput = { cart_id: string; ids: string[] }
// TODO: The DeleteLineItemsWorkflow are missing the following steps:
// - Refresh/delete shipping methods (fulfillment module)
@@ -12,6 +13,8 @@ export const deleteLineItemsWorkflowId = "delete-line-items"
export const deleteLineItemsWorkflow = createWorkflow(
deleteLineItemsWorkflowId,
(input: WorkflowData<WorkflowInput>) => {
return deleteLineItemsStep(input.ids)
deleteLineItemsStep(input.ids)
refreshCartPromotionsStep({ id: input.cart_id })
}
)

View File

@@ -200,6 +200,11 @@ export interface ComputeActionShippingLine extends Record<string, unknown> {
* The context provided when computing actions of promotions.
*/
export interface ComputeActionContext extends Record<string, unknown> {
/**
* The cart's currency
*/
currency_code: string
/**
* The cart's line items.
*/

View File

@@ -2,12 +2,12 @@ import {
deleteLineItemsWorkflow,
updateLineItemInCartWorkflow,
} from "@medusajs/core-flows"
import { DeleteResponse } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
import { prepareListQuery } from "../../../../../../utils/get-query-config"
import { refetchCart } from "../../../helpers"
import { StoreUpdateCartLineItemType } from "../../../validators"
import { prepareListQuery } from "../../../../../../utils/get-query-config"
import { DeleteResponse } from "@medusajs/types"
export const POST = async (
req: MedusaRequest<StoreUpdateCartLineItemType>,
@@ -70,7 +70,7 @@ export const DELETE = async (
const id = req.params.line_id
const { errors } = await deleteLineItemsWorkflow(req.scope).run({
input: { ids: [id] },
input: { cart_id: req.params.id, ids: [id] },
throwOnError: false,
})