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:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
)
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.*",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user