Skip to content

Commit 79582bc

Browse files
authored
fix(core-flows): prevent completing cart from webhook when order already exists (medusajs#14108)
* Prevent completing cart when order already exists in processPaymentWorkflow * Add tests * Add changeset
1 parent d6a9e36 commit 79582bc

File tree

3 files changed

+206
-3
lines changed

3 files changed

+206
-3
lines changed

.changeset/plain-windows-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/core-flows": patch
3+
---
4+
5+
fix(core-flows): prevent completing cart from webhook when order already exists

integration-tests/modules/__tests__/cart/store/cart.completion.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import {
22
addToCartWorkflow,
3+
beginOrderEditOrderWorkflow,
34
completeCartWorkflow,
5+
confirmOrderEditRequestWorkflow,
46
createCartWorkflow,
57
createPaymentCollectionForCartWorkflow,
68
createPaymentSessionsWorkflow,
79
getOrderDetailWorkflow,
810
listShippingOptionsForCartWorkflow,
11+
orderEditAddNewItemWorkflow,
912
processPaymentWorkflow,
1013
} from "@medusajs/core-flows"
1114
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
@@ -26,6 +29,7 @@ import {
2629
import {
2730
ContainerRegistrationKeys,
2831
Modules,
32+
PaymentCollectionStatus,
2933
ProductStatus,
3034
remoteQueryObjectFromString,
3135
} from "@medusajs/utils"
@@ -1024,6 +1028,187 @@ medusaIntegrationTestRunner({
10241028
"Failed to do something before ending complete cart workflow"
10251029
)
10261030
})
1031+
1032+
it("should avoid completing cart when order already exists", async () => {
1033+
const salesChannel = await scModuleService.createSalesChannels({
1034+
name: "Webshop",
1035+
})
1036+
1037+
const location = await stockLocationModule.createStockLocations({
1038+
name: "Warehouse",
1039+
})
1040+
1041+
const [product] = await productModule.createProducts([
1042+
{
1043+
title: "Test product",
1044+
status: ProductStatus.PUBLISHED,
1045+
variants: [
1046+
{
1047+
title: "Test variant",
1048+
manage_inventory: false,
1049+
},
1050+
],
1051+
},
1052+
])
1053+
1054+
const priceSet = await pricingModule.createPriceSets({
1055+
prices: [
1056+
{
1057+
amount: 3000,
1058+
currency_code: "usd",
1059+
},
1060+
],
1061+
})
1062+
1063+
await pricingModule.createPricePreferences({
1064+
attribute: "currency_code",
1065+
value: "usd",
1066+
has_tax_inclusive: true,
1067+
})
1068+
1069+
await remoteLink.create([
1070+
{
1071+
[Modules.PRODUCT]: {
1072+
variant_id: product.variants[0].id,
1073+
},
1074+
[Modules.PRICING]: {
1075+
price_set_id: priceSet.id,
1076+
},
1077+
},
1078+
{
1079+
[Modules.SALES_CHANNEL]: {
1080+
sales_channel_id: salesChannel.id,
1081+
},
1082+
[Modules.STOCK_LOCATION]: {
1083+
stock_location_id: location.id,
1084+
},
1085+
},
1086+
])
1087+
1088+
// create cart
1089+
const cart = await cartModuleService.createCarts({
1090+
currency_code: "usd",
1091+
sales_channel_id: salesChannel.id,
1092+
})
1093+
1094+
await addToCartWorkflow(appContainer).run({
1095+
input: {
1096+
items: [
1097+
{
1098+
variant_id: product.variants[0].id,
1099+
quantity: 1,
1100+
requires_shipping: false,
1101+
},
1102+
],
1103+
cart_id: cart.id,
1104+
},
1105+
})
1106+
1107+
await createPaymentCollectionForCartWorkflow(appContainer).run({
1108+
input: {
1109+
cart_id: cart.id,
1110+
},
1111+
})
1112+
1113+
const [payCol] = await remoteQuery(
1114+
remoteQueryObjectFromString({
1115+
entryPoint: "cart_payment_collection",
1116+
variables: { filters: { cart_id: cart.id } },
1117+
fields: ["payment_collection_id"],
1118+
})
1119+
)
1120+
1121+
await createPaymentSessionsWorkflow(appContainer).run({
1122+
input: {
1123+
payment_collection_id: payCol.payment_collection_id,
1124+
provider_id: "pp_system_default",
1125+
context: {},
1126+
data: {},
1127+
},
1128+
})
1129+
1130+
const {
1131+
result: { id: orderId },
1132+
} = await completeCartWorkflow(appContainer).run({
1133+
input: {
1134+
id: cart.id,
1135+
},
1136+
})
1137+
1138+
await beginOrderEditOrderWorkflow(appContainer).run({
1139+
input: {
1140+
order_id: orderId,
1141+
},
1142+
})
1143+
await orderEditAddNewItemWorkflow(appContainer).run({
1144+
input: {
1145+
order_id: orderId,
1146+
items: [
1147+
{
1148+
variant_id: product.variants[0].id,
1149+
quantity: 1,
1150+
},
1151+
],
1152+
},
1153+
})
1154+
await confirmOrderEditRequestWorkflow(appContainer).run({
1155+
input: { order_id: orderId },
1156+
})
1157+
1158+
const orderPaymentCollections = await remoteQuery(
1159+
remoteQueryObjectFromString({
1160+
entryPoint: "order_payment_collection",
1161+
variables: { filters: { order_id: orderId } },
1162+
fields: ["payment_collection_id"],
1163+
})
1164+
)
1165+
1166+
const [pendingPaymentCollection] = await remoteQuery(
1167+
remoteQueryObjectFromString({
1168+
entryPoint: "payment_collection",
1169+
variables: {
1170+
filters: {
1171+
id: orderPaymentCollections.map(
1172+
(orderPayCol) => orderPayCol.payment_collection_id
1173+
),
1174+
status: PaymentCollectionStatus.NOT_PAID,
1175+
},
1176+
},
1177+
fields: ["id"],
1178+
})
1179+
)
1180+
1181+
const { result: paymentSession } =
1182+
await createPaymentSessionsWorkflow(appContainer).run({
1183+
input: {
1184+
payment_collection_id: pendingPaymentCollection.id,
1185+
provider_id: "pp_system_default",
1186+
context: {},
1187+
data: {},
1188+
},
1189+
})
1190+
1191+
let completeCartCalled = false
1192+
const workflow = processPaymentWorkflow(appContainer)
1193+
1194+
workflow.addAction("track-complete-cart-step", {
1195+
invoke: async function trackStep({ invoke }) {
1196+
completeCartCalled = !!invoke["complete-cart-after-payment-step"]
1197+
},
1198+
})
1199+
1200+
await workflow.run({
1201+
input: {
1202+
action: "captured",
1203+
data: {
1204+
session_id: paymentSession.id,
1205+
amount: 3000,
1206+
},
1207+
},
1208+
})
1209+
1210+
expect(completeCartCalled).toBe(false)
1211+
})
10271212
})
10281213
})
10291214
},

packages/core/core-flows/src/payment/workflows/process-payment.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,23 @@ export const processPaymentWorkflow = createWorkflow(
7373
const cartId = transform(
7474
{ cartPaymentCollection },
7575
({ cartPaymentCollection }) => {
76-
return cartPaymentCollection.data[0].cart_id
76+
return cartPaymentCollection.data[0]?.cart_id
7777
}
7878
)
7979

80+
const { data: order } = useQueryGraphStep({
81+
entity: "order_cart",
82+
fields: ["id"],
83+
filters: {
84+
cart_id: cartId
85+
},
86+
options: {
87+
isList: false
88+
}
89+
}).config({
90+
name: "cart-order-query",
91+
})
92+
8093
when("lock-cart-when-available", { cartId }, ({ cartId }) => {
8194
return !!cartId
8295
}).then(() => {
@@ -158,8 +171,8 @@ export const processPaymentWorkflow = createWorkflow(
158171
})
159172
})
160173

161-
when({ cartPaymentCollection }, ({ cartPaymentCollection }) => {
162-
return !!cartPaymentCollection.data.length
174+
when({ cartPaymentCollection, order }, ({ cartPaymentCollection, order }) => {
175+
return !!cartPaymentCollection.data.length && !order
163176
}).then(() => {
164177
completeCartAfterPaymentStep({
165178
cart_id: cartPaymentCollection.data[0].cart_id,

0 commit comments

Comments
 (0)