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 authored and GitHub committed 2024-05-24 09:48:14 +00:00
1 parent d29d5436fb
commit ff870482bb
10 files changed
+122 -49

No files matched your search

@@ -1,9 +1,10 @@
import { LinkModuleUtils, ModuleRegistrationName } from "@medusajs/modules-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { CartDTO, IPromotionModuleService } from "@medusajs/types" import { CartDTO, IPromotionModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk" import { StepResponse, createStep } from "@medusajs/workflows-sdk"
interface StepInput { interface StepInput {
cart: CartDTO cart: CartDTO
promotionCodesToApply: string[]
} }
export const getActionsToComputeFromPromotionsStepId = export const getActionsToComputeFromPromotionsStepId =
@@ -11,26 +12,13 @@ export const getActionsToComputeFromPromotionsStepId =
export const getActionsToComputeFromPromotionsStep = createStep( export const getActionsToComputeFromPromotionsStep = createStep(
getActionsToComputeFromPromotionsStepId, getActionsToComputeFromPromotionsStepId,
async (data: StepInput, { container }) => { async (data: StepInput, { container }) => {
const { cart } = data const { cart, promotionCodesToApply = [] } = data
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const promotionService = container.resolve<IPromotionModuleService>( const promotionService = container.resolve<IPromotionModuleService>(
ModuleRegistrationName.PROMOTION 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( const actionsToCompute = await promotionService.computeActions(
existingPromotions.map((p) => p.code!), promotionCodesToApply,
cart as any 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 "./find-sales-channel"
export * from "./get-actions-to-compute-from-promotions" export * from "./get-actions-to-compute-from-promotions"
export * from "./get-item-tax-lines" export * from "./get-item-tax-lines"
export * from "./get-promotion-codes-to-apply"
export * from "./get-variant-price-sets" export * from "./get-variant-price-sets"
export * from "./get-variants" export * from "./get-variants"
export * from "./prepare-adjustments-from-promotion-actions" export * from "./prepare-adjustments-from-promotion-actions"
@@ -65,11 +65,17 @@ export const prepareAdjustmentsFromPromotionActionsStep = createStep(
) )
.map((a) => (a as RemoveShippingMethodAdjustment).adjustment_id) .map((a) => (a as RemoveShippingMethodAdjustment).adjustment_id)
const computedPromotionCodes = [
...lineItemAdjustmentsToCreate,
...shippingMethodAdjustmentsToCreate,
].map((adjustment) => adjustment.code)
return new StepResponse({ return new StepResponse({
lineItemAdjustmentsToCreate, lineItemAdjustmentsToCreate,
lineItemAdjustmentIdsToRemove, lineItemAdjustmentIdsToRemove,
shippingMethodAdjustmentsToCreate, shippingMethodAdjustmentsToCreate,
shippingMethodAdjustmentIdsToRemove, shippingMethodAdjustmentIdsToRemove,
computedPromotionCodes,
}) })
} }
) )
@@ -21,7 +21,6 @@ export const updateCartPromotionsStep = createStep(
updateCartPromotionsStepId, updateCartPromotionsStepId,
async (data: StepInput, { container }) => { async (data: StepInput, { container }) => {
const { promo_codes = [], id, action = PromotionActions.ADD } = data const { promo_codes = [], id, action = PromotionActions.ADD } = data
const remoteLink = container.resolve(LinkModuleUtils.REMOTE_LINK) const remoteLink = container.resolve(LinkModuleUtils.REMOTE_LINK)
const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY) const remoteQuery = container.resolve(LinkModuleUtils.REMOTE_QUERY)
const promotionService = container.resolve<IPromotionModuleService>( const promotionService = container.resolve<IPromotionModuleService>(
@@ -75,19 +74,14 @@ export const updateCartPromotionsStep = createStep(
} }
} }
const linksToDismissPromise = linksToDismiss.length if (linksToDismiss.length) {
? remoteLink.dismiss(linksToDismiss) await remoteLink.dismiss(linksToDismiss)
: [] }
const linksToCreatePromise = linksToCreate.length const createdLinks = linksToCreate.length
? remoteLink.create(linksToCreate) ? await remoteLink.create(linksToCreate)
: [] : []
const [_, createdLinks] = await Promise.all([
linksToDismissPromise,
linksToCreatePromise,
])
return new StepResponse(null, { return new StepResponse(null, {
createdLinkIds: createdLinks.map((link) => link.id), createdLinkIds: createdLinks.map((link) => link.id),
dismissedLinks: linksToDismiss, dismissedLinks: linksToDismiss,
@@ -1,12 +1,21 @@
export const cartFieldsForRefreshSteps = [ export const cartFieldsForRefreshSteps = [
"subtotal",
"item_subtotal",
"shipping_subtotal",
"region_id", "region_id",
"currency_code", "currency_code",
"region.*", "region.*",
"items.*", "items.*",
"items.product.id",
"items.product.collection_id",
"items.product.categories.id",
"items.product.tags.id",
"items.adjustments.*",
"items.tax_lines.*", "items.tax_lines.*",
"shipping_address.*", "shipping_address.*",
"shipping_methods.*", "shipping_methods.*",
"shipping_methods.tax_lines*", "shipping_methods.adjustments.*",
"shipping_methods.tax_lines.*",
"customer.*", "customer.*",
"customer.groups.*", "customer.groups.*",
] ]
@@ -4,16 +4,18 @@ import {
createWorkflow, createWorkflow,
parallelize, parallelize,
} from "@medusajs/workflows-sdk" } from "@medusajs/workflows-sdk"
import { useRemoteQueryStep } from "../../../common"
import { import {
createLineItemAdjustmentsStep, createLineItemAdjustmentsStep,
createShippingMethodAdjustmentsStep, createShippingMethodAdjustmentsStep,
getActionsToComputeFromPromotionsStep, getActionsToComputeFromPromotionsStep,
getPromotionCodesToApply,
prepareAdjustmentsFromPromotionActionsStep, prepareAdjustmentsFromPromotionActionsStep,
removeLineItemAdjustmentsStep, removeLineItemAdjustmentsStep,
removeShippingMethodAdjustmentsStep, removeShippingMethodAdjustmentsStep,
retrieveCartStep,
updateCartPromotionsStep,
} from "../steps" } from "../steps"
import { updateCartPromotionsStep } from "../steps/update-cart-promotions"
import { cartFieldsForRefreshSteps } from "../utils/fields"
type WorkflowInput = { type WorkflowInput = {
promoCodes: string[] promoCodes: string[]
@@ -28,27 +30,22 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions"
export const updateCartPromotionsWorkflow = createWorkflow( export const updateCartPromotionsWorkflow = createWorkflow(
updateCartPromotionsWorkflowId, updateCartPromotionsWorkflowId,
(input: WorkflowData<WorkflowInput>): WorkflowData<void> => { (input: WorkflowData<WorkflowInput>): WorkflowData<void> => {
const retrieveCartInput = { const cart = useRemoteQueryStep({
id: input.cartId, entry_point: "cart",
config: { fields: cartFieldsForRefreshSteps,
relations: [ variables: { id: input.cartId },
"items", list: false,
"items.adjustments", })
"shipping_methods",
"shipping_methods.adjustments",
],
},
}
updateCartPromotionsStep({ const promotionCodesToApply = getPromotionCodesToApply({
id: input.cartId, cart: cart,
promo_codes: input.promoCodes, promo_codes: input.promoCodes,
action: input.action || PromotionActions.ADD, action: input.action || PromotionActions.ADD,
}) })
const cart = retrieveCartStep(retrieveCartInput)
const actions = getActionsToComputeFromPromotionsStep({ const actions = getActionsToComputeFromPromotionsStep({
cart, cart,
promotionCodesToApply,
}) })
const { const {
@@ -56,6 +53,7 @@ export const updateCartPromotionsWorkflow = createWorkflow(
lineItemAdjustmentIdsToRemove, lineItemAdjustmentIdsToRemove,
shippingMethodAdjustmentsToCreate, shippingMethodAdjustmentsToCreate,
shippingMethodAdjustmentIdsToRemove, shippingMethodAdjustmentIdsToRemove,
computedPromotionCodes,
} = prepareAdjustmentsFromPromotionActionsStep({ actions }) } = prepareAdjustmentsFromPromotionActionsStep({ actions })
parallelize( parallelize(
@@ -69,5 +67,11 @@ export const updateCartPromotionsWorkflow = createWorkflow(
createLineItemAdjustmentsStep({ lineItemAdjustmentsToCreate }), createLineItemAdjustmentsStep({ lineItemAdjustmentsToCreate }),
createShippingMethodAdjustmentsStep({ shippingMethodAdjustmentsToCreate }) createShippingMethodAdjustmentsStep({ shippingMethodAdjustmentsToCreate })
) )
updateCartPromotionsStep({
id: input.cartId,
promo_codes: computedPromotionCodes,
action: PromotionActions.REPLACE,
})
} }
) )
@@ -1,7 +1,8 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { refreshCartPromotionsStep } from "../../cart/steps/refresh-cart-promotions"
import { deleteLineItemsStep } from "../steps/delete-line-items" 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: // TODO: The DeleteLineItemsWorkflow are missing the following steps:
// - Refresh/delete shipping methods (fulfillment module) // - Refresh/delete shipping methods (fulfillment module)
@@ -12,6 +13,8 @@ export const deleteLineItemsWorkflowId = "delete-line-items"
export const deleteLineItemsWorkflow = createWorkflow( export const deleteLineItemsWorkflow = createWorkflow(
deleteLineItemsWorkflowId, deleteLineItemsWorkflowId,
(input: WorkflowData<WorkflowInput>) => { (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. * The context provided when computing actions of promotions.
*/ */
export interface ComputeActionContext extends Record<string, unknown> { export interface ComputeActionContext extends Record<string, unknown> {
/**
* The cart's currency
*/
currency_code: string
/** /**
* The cart's line items. * The cart's line items.
*/ */
@@ -2,12 +2,12 @@ import {
deleteLineItemsWorkflow, deleteLineItemsWorkflow,
updateLineItemInCartWorkflow, updateLineItemInCartWorkflow,
} from "@medusajs/core-flows" } from "@medusajs/core-flows"
import { DeleteResponse } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils" import { MedusaError } from "@medusajs/utils"
import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing" import { MedusaRequest, MedusaResponse } from "../../../../../../types/routing"
import { prepareListQuery } from "../../../../../../utils/get-query-config"
import { refetchCart } from "../../../helpers" import { refetchCart } from "../../../helpers"
import { StoreUpdateCartLineItemType } from "../../../validators" import { StoreUpdateCartLineItemType } from "../../../validators"
import { prepareListQuery } from "../../../../../../utils/get-query-config"
import { DeleteResponse } from "@medusajs/types"
export const POST = async ( export const POST = async (
req: MedusaRequest<StoreUpdateCartLineItemType>, req: MedusaRequest<StoreUpdateCartLineItemType>,
@@ -70,7 +70,7 @@ export const DELETE = async (
const id = req.params.line_id const id = req.params.line_id
const { errors } = await deleteLineItemsWorkflow(req.scope).run({ const { errors } = await deleteLineItemsWorkflow(req.scope).run({
input: { ids: [id] }, input: { cart_id: req.params.id, ids: [id] },
throwOnError: false, throwOnError: false,
}) })