From ff870482bb84218a95f82964fcd0a352f7a93281 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 24 May 2024 11:48:14 +0200 Subject: [PATCH] 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. --- .../get-actions-to-compute-from-promotions.ts | 20 ++---- .../steps/get-promotion-codes-to-apply.ts | 63 +++++++++++++++++++ .../src/definition/cart/steps/index.ts | 1 + ...pare-adjustments-from-promotion-actions.ts | 6 ++ .../cart/steps/update-cart-promotions.ts | 16 ++--- .../src/definition/cart/utils/fields.ts | 11 +++- .../cart/workflows/update-cart-promotions.ts | 36 ++++++----- .../line-item/workflows/delete-line-items.ts | 7 ++- .../src/promotion/common/compute-actions.ts | 5 ++ .../carts/[id]/line-items/[line_id]/route.ts | 6 +- 10 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 packages/core/core-flows/src/definition/cart/steps/get-promotion-codes-to-apply.ts diff --git a/packages/core/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts b/packages/core/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts index b1eebded3e..3a8de587f0 100644 --- a/packages/core/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts +++ b/packages/core/core-flows/src/definition/cart/steps/get-actions-to-compute-from-promotions.ts @@ -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( 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 ) diff --git a/packages/core/core-flows/src/definition/cart/steps/get-promotion-codes-to-apply.ts b/packages/core/core-flows/src/definition/cart/steps/get-promotion-codes-to-apply.ts new file mode 100644 index 0000000000..8c28a086cf --- /dev/null +++ b/packages/core/core-flows/src/definition/cart/steps/get-promotion-codes-to-apply.ts @@ -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( + 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 = 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)) + } +) diff --git a/packages/core/core-flows/src/definition/cart/steps/index.ts b/packages/core/core-flows/src/definition/cart/steps/index.ts index 8efcab9ae5..cb6f553a9c 100644 --- a/packages/core/core-flows/src/definition/cart/steps/index.ts +++ b/packages/core/core-flows/src/definition/cart/steps/index.ts @@ -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" diff --git a/packages/core/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts b/packages/core/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts index 9c12cc9905..6511ba1dd9 100644 --- a/packages/core/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts +++ b/packages/core/core-flows/src/definition/cart/steps/prepare-adjustments-from-promotion-actions.ts @@ -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, }) } ) diff --git a/packages/core/core-flows/src/definition/cart/steps/update-cart-promotions.ts b/packages/core/core-flows/src/definition/cart/steps/update-cart-promotions.ts index 6411646098..ad4a19ffaa 100644 --- a/packages/core/core-flows/src/definition/cart/steps/update-cart-promotions.ts +++ b/packages/core/core-flows/src/definition/cart/steps/update-cart-promotions.ts @@ -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( @@ -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, diff --git a/packages/core/core-flows/src/definition/cart/utils/fields.ts b/packages/core/core-flows/src/definition/cart/utils/fields.ts index d9e63d4116..4b66ff3729 100644 --- a/packages/core/core-flows/src/definition/cart/utils/fields.ts +++ b/packages/core/core-flows/src/definition/cart/utils/fields.ts @@ -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.*", ] diff --git a/packages/core/core-flows/src/definition/cart/workflows/update-cart-promotions.ts b/packages/core/core-flows/src/definition/cart/workflows/update-cart-promotions.ts index 78022217c9..ebfa13ac43 100644 --- a/packages/core/core-flows/src/definition/cart/workflows/update-cart-promotions.ts +++ b/packages/core/core-flows/src/definition/cart/workflows/update-cart-promotions.ts @@ -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): WorkflowData => { - 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, + }) } ) diff --git a/packages/core/core-flows/src/definition/line-item/workflows/delete-line-items.ts b/packages/core/core-flows/src/definition/line-item/workflows/delete-line-items.ts index 5065dd588a..55533ea048 100644 --- a/packages/core/core-flows/src/definition/line-item/workflows/delete-line-items.ts +++ b/packages/core/core-flows/src/definition/line-item/workflows/delete-line-items.ts @@ -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) => { - return deleteLineItemsStep(input.ids) + deleteLineItemsStep(input.ids) + + refreshCartPromotionsStep({ id: input.cart_id }) } ) diff --git a/packages/core/types/src/promotion/common/compute-actions.ts b/packages/core/types/src/promotion/common/compute-actions.ts index c602b34945..3cd45007ad 100644 --- a/packages/core/types/src/promotion/common/compute-actions.ts +++ b/packages/core/types/src/promotion/common/compute-actions.ts @@ -200,6 +200,11 @@ export interface ComputeActionShippingLine extends Record { * The context provided when computing actions of promotions. */ export interface ComputeActionContext extends Record { + /** + * The cart's currency + */ + currency_code: string + /** * The cart's line items. */ diff --git a/packages/medusa/src/api/store/carts/[id]/line-items/[line_id]/route.ts b/packages/medusa/src/api/store/carts/[id]/line-items/[line_id]/route.ts index 3244745080..2286fcab48 100644 --- a/packages/medusa/src/api/store/carts/[id]/line-items/[line_id]/route.ts +++ b/packages/medusa/src/api/store/carts/[id]/line-items/[line_id]/route.ts @@ -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, @@ -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, })