From 79582bc94eb420f02ad49a81bcbcc02521e5c414 Mon Sep 17 00:00:00 2001 From: Nicolas Gorga <62995075+NicolasGorga@users.noreply.github.com> Date: Mon, 24 Nov 2025 05:23:39 -0300 Subject: [PATCH] fix(core-flows): prevent completing cart from webhook when order already exists (#14108) * Prevent completing cart when order already exists in processPaymentWorkflow * Add tests * Add changeset --- .changeset/plain-windows-end.md | 5 + .../__tests__/cart/store/cart.completion.ts | 185 ++++++++++++++++++ .../src/payment/workflows/process-payment.ts | 19 +- 3 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 .changeset/plain-windows-end.md diff --git a/.changeset/plain-windows-end.md b/.changeset/plain-windows-end.md new file mode 100644 index 0000000000..e006dc21b1 --- /dev/null +++ b/.changeset/plain-windows-end.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows): prevent completing cart from webhook when order already exists diff --git a/integration-tests/modules/__tests__/cart/store/cart.completion.ts b/integration-tests/modules/__tests__/cart/store/cart.completion.ts index 98505c8cfc..1f3bf784d9 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.completion.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.completion.ts @@ -1,11 +1,14 @@ import { addToCartWorkflow, + beginOrderEditOrderWorkflow, completeCartWorkflow, + confirmOrderEditRequestWorkflow, createCartWorkflow, createPaymentCollectionForCartWorkflow, createPaymentSessionsWorkflow, getOrderDetailWorkflow, listShippingOptionsForCartWorkflow, + orderEditAddNewItemWorkflow, processPaymentWorkflow, } from "@medusajs/core-flows" import { medusaIntegrationTestRunner } from "@medusajs/test-utils" @@ -26,6 +29,7 @@ import { import { ContainerRegistrationKeys, Modules, + PaymentCollectionStatus, ProductStatus, remoteQueryObjectFromString, } from "@medusajs/utils" @@ -1024,6 +1028,187 @@ medusaIntegrationTestRunner({ "Failed to do something before ending complete cart workflow" ) }) + + it("should avoid completing cart when order already exists", async () => { + const salesChannel = await scModuleService.createSalesChannels({ + name: "Webshop", + }) + + const location = await stockLocationModule.createStockLocations({ + name: "Warehouse", + }) + + const [product] = await productModule.createProducts([ + { + title: "Test product", + status: ProductStatus.PUBLISHED, + variants: [ + { + title: "Test variant", + manage_inventory: false, + }, + ], + }, + ]) + + const priceSet = await pricingModule.createPriceSets({ + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + }) + + await pricingModule.createPricePreferences({ + attribute: "currency_code", + value: "usd", + has_tax_inclusive: true, + }) + + await remoteLink.create([ + { + [Modules.PRODUCT]: { + variant_id: product.variants[0].id, + }, + [Modules.PRICING]: { + price_set_id: priceSet.id, + }, + }, + { + [Modules.SALES_CHANNEL]: { + sales_channel_id: salesChannel.id, + }, + [Modules.STOCK_LOCATION]: { + stock_location_id: location.id, + }, + }, + ]) + + // create cart + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + sales_channel_id: salesChannel.id, + }) + + await addToCartWorkflow(appContainer).run({ + input: { + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + requires_shipping: false, + }, + ], + cart_id: cart.id, + }, + }) + + await createPaymentCollectionForCartWorkflow(appContainer).run({ + input: { + cart_id: cart.id, + }, + }) + + const [payCol] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "cart_payment_collection", + variables: { filters: { cart_id: cart.id } }, + fields: ["payment_collection_id"], + }) + ) + + await createPaymentSessionsWorkflow(appContainer).run({ + input: { + payment_collection_id: payCol.payment_collection_id, + provider_id: "pp_system_default", + context: {}, + data: {}, + }, + }) + + const { + result: { id: orderId }, + } = await completeCartWorkflow(appContainer).run({ + input: { + id: cart.id, + }, + }) + + await beginOrderEditOrderWorkflow(appContainer).run({ + input: { + order_id: orderId, + }, + }) + await orderEditAddNewItemWorkflow(appContainer).run({ + input: { + order_id: orderId, + items: [ + { + variant_id: product.variants[0].id, + quantity: 1, + }, + ], + }, + }) + await confirmOrderEditRequestWorkflow(appContainer).run({ + input: { order_id: orderId }, + }) + + const orderPaymentCollections = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "order_payment_collection", + variables: { filters: { order_id: orderId } }, + fields: ["payment_collection_id"], + }) + ) + + const [pendingPaymentCollection] = await remoteQuery( + remoteQueryObjectFromString({ + entryPoint: "payment_collection", + variables: { + filters: { + id: orderPaymentCollections.map( + (orderPayCol) => orderPayCol.payment_collection_id + ), + status: PaymentCollectionStatus.NOT_PAID, + }, + }, + fields: ["id"], + }) + ) + + const { result: paymentSession } = + await createPaymentSessionsWorkflow(appContainer).run({ + input: { + payment_collection_id: pendingPaymentCollection.id, + provider_id: "pp_system_default", + context: {}, + data: {}, + }, + }) + + let completeCartCalled = false + const workflow = processPaymentWorkflow(appContainer) + + workflow.addAction("track-complete-cart-step", { + invoke: async function trackStep({ invoke }) { + completeCartCalled = !!invoke["complete-cart-after-payment-step"] + }, + }) + + await workflow.run({ + input: { + action: "captured", + data: { + session_id: paymentSession.id, + amount: 3000, + }, + }, + }) + + expect(completeCartCalled).toBe(false) + }) }) }) }, diff --git a/packages/core/core-flows/src/payment/workflows/process-payment.ts b/packages/core/core-flows/src/payment/workflows/process-payment.ts index 9210cfa468..bf219042af 100644 --- a/packages/core/core-flows/src/payment/workflows/process-payment.ts +++ b/packages/core/core-flows/src/payment/workflows/process-payment.ts @@ -73,10 +73,23 @@ export const processPaymentWorkflow = createWorkflow( const cartId = transform( { cartPaymentCollection }, ({ cartPaymentCollection }) => { - return cartPaymentCollection.data[0].cart_id + return cartPaymentCollection.data[0]?.cart_id } ) + const { data: order } = useQueryGraphStep({ + entity: "order_cart", + fields: ["id"], + filters: { + cart_id: cartId + }, + options: { + isList: false + } + }).config({ + name: "cart-order-query", + }) + when("lock-cart-when-available", { cartId }, ({ cartId }) => { return !!cartId }).then(() => { @@ -158,8 +171,8 @@ export const processPaymentWorkflow = createWorkflow( }) }) - when({ cartPaymentCollection }, ({ cartPaymentCollection }) => { - return !!cartPaymentCollection.data.length + when({ cartPaymentCollection, order }, ({ cartPaymentCollection, order }) => { + return !!cartPaymentCollection.data.length && !order }).then(() => { completeCartAfterPaymentStep({ cart_id: cartPaymentCollection.data[0].cart_id,