fix(core-flows): complete cart improvements (#12646)
* fix(core-flows): use cartId as transactionId and acquire lock to complete cart * fix cart update compensation
This commit is contained in:
@@ -29,7 +29,18 @@ export const updateCartsStep = createStep(
|
||||
async (data: UpdateCartsStepInput, { container }) => {
|
||||
const cartModule = container.resolve<ICartModuleService>(Modules.CART)
|
||||
|
||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray(data)
|
||||
const { selects, relations } = getSelectsAndRelationsFromObjectArray(data, {
|
||||
requiredFields: [
|
||||
"id",
|
||||
"region_id",
|
||||
"customer_id",
|
||||
"sales_channel_id",
|
||||
"email",
|
||||
"currency_code",
|
||||
"metadata",
|
||||
"completed_at",
|
||||
],
|
||||
})
|
||||
const cartsBeforeUpdate = await cartModule.listCarts(
|
||||
{ id: data.map((d) => d.id) },
|
||||
{ select: selects, relations }
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
useQueryGraphStep,
|
||||
useRemoteQueryStep,
|
||||
} from "../../common"
|
||||
import { acquireLockStep } from "../../locking/acquire-lock"
|
||||
import { releaseLockStep } from "../../locking/release-lock"
|
||||
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
||||
import { createOrdersStep } from "../../order/steps/create-orders"
|
||||
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
|
||||
@@ -60,7 +62,9 @@ export type CompleteCartWorkflowOutput = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const THREE_DAYS = 60 * 60 * 24 * 3
|
||||
const THREE_DAYS = 60 * 60 * 24 * 3
|
||||
const THIRTY_SECONDS = 30
|
||||
const TWO_MINUTES = 60 * 2
|
||||
|
||||
export const completeCartWorkflowId = "complete-cart"
|
||||
/**
|
||||
@@ -93,6 +97,12 @@ export const completeCartWorkflow = createWorkflow(
|
||||
retentionTime: THREE_DAYS,
|
||||
},
|
||||
(input: WorkflowData<CompleteCartWorkflowInput>) => {
|
||||
acquireLockStep({
|
||||
key: input.id,
|
||||
timeout: THIRTY_SECONDS,
|
||||
ttl: TWO_MINUTES,
|
||||
})
|
||||
|
||||
const orderCart = useQueryGraphStep({
|
||||
entity: "order_cart",
|
||||
fields: ["cart_id", "order_id"],
|
||||
@@ -381,6 +391,10 @@ export const completeCartWorkflow = createWorkflow(
|
||||
return createdOrder
|
||||
})
|
||||
|
||||
releaseLockStep({
|
||||
key: input.id,
|
||||
})
|
||||
|
||||
const result = transform({ order, orderId }, ({ order, orderId }) => {
|
||||
return { id: order?.id ?? orderId } as CompleteCartWorkflowOutput
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from "./fulfillment"
|
||||
export * from "./inventory"
|
||||
export * from "./invite"
|
||||
export * from "./line-item"
|
||||
export * from "./locking"
|
||||
export * from "./notification"
|
||||
export * from "./order"
|
||||
export * from "./payment"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { isDefined, Modules } from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
import { setTimeout } from "timers/promises"
|
||||
|
||||
/**
|
||||
* The keys to be locked
|
||||
*/
|
||||
export interface AcquireLockStepInput {
|
||||
key: string | string[]
|
||||
timeout?: number // in seconds. Defaults to 0
|
||||
retryInterval?: number // in seconds. Defaults to 0.3
|
||||
ttl?: number // in seconds
|
||||
ownerId?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
export const acquireLockStepId = "acquire-lock-step"
|
||||
/**
|
||||
* This step acquires a lock for a given key.
|
||||
*
|
||||
* @example
|
||||
* const data = acquireLockStep({
|
||||
* "key": "my-lock-key",
|
||||
* "ttl": 60
|
||||
* })
|
||||
*/
|
||||
export const acquireLockStep = createStep(
|
||||
acquireLockStepId,
|
||||
async (data: AcquireLockStepInput, { container }) => {
|
||||
const keys = Array.isArray(data.key)
|
||||
? data.key
|
||||
: isDefined(data.key)
|
||||
? [data.key]
|
||||
: []
|
||||
|
||||
if (!keys.length) {
|
||||
return new StepResponse(void 0)
|
||||
}
|
||||
|
||||
const locking = container.resolve(Modules.LOCKING)
|
||||
|
||||
const retryInterval = data.retryInterval ?? 0.3
|
||||
const tryUntil = Date.now() + (data.timeout ?? 0) * 1000
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
await locking.acquire(data.key, {
|
||||
expire: data.ttl,
|
||||
ownerId: data.ownerId,
|
||||
provider: data.provider,
|
||||
})
|
||||
break
|
||||
} catch (e) {
|
||||
if (Date.now() >= tryUntil) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
await setTimeout(retryInterval * 1000)
|
||||
}
|
||||
|
||||
return new StepResponse(void 0, {
|
||||
keys,
|
||||
ownerId: data.ownerId,
|
||||
provider: data.provider,
|
||||
})
|
||||
},
|
||||
async (data, { container }) => {
|
||||
if (!data?.keys?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const locking = container.resolve(Modules.LOCKING)
|
||||
|
||||
await locking.release(data.keys, {
|
||||
ownerId: data.ownerId,
|
||||
provider: data.provider,
|
||||
})
|
||||
|
||||
return new StepResponse()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./acquire-lock"
|
||||
export * from "./release-lock"
|
||||
@@ -0,0 +1,43 @@
|
||||
import { isDefined, Modules } from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
* The locked keys to be released
|
||||
*/
|
||||
export interface ReleaseLockStepInput {
|
||||
key: string | string[]
|
||||
ownerId?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
export const releaseLockStepId = "release-lock-step"
|
||||
/**
|
||||
* This step releases a lock for a given key.
|
||||
*
|
||||
* @example
|
||||
* const data = releaseLockStep({
|
||||
* "key": "my-lock-key"
|
||||
* })
|
||||
*/
|
||||
export const releaseLockStep = createStep(
|
||||
releaseLockStepId,
|
||||
async (data: ReleaseLockStepInput, { container }) => {
|
||||
const keys = Array.isArray(data.key)
|
||||
? data.key
|
||||
: isDefined(data.key)
|
||||
? [data.key]
|
||||
: []
|
||||
|
||||
if (!keys.length) {
|
||||
return new StepResponse(true)
|
||||
}
|
||||
|
||||
const locking = container.resolve(Modules.LOCKING)
|
||||
const released = await locking.release(keys, {
|
||||
ownerId: data.ownerId,
|
||||
provider: data.provider,
|
||||
})
|
||||
|
||||
return new StepResponse(released)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Modules } from "@medusajs/framework/utils"
|
||||
import { createStep } from "@medusajs/framework/workflows-sdk"
|
||||
import { completeCartWorkflowId } from "../../cart/workflows/complete-cart"
|
||||
|
||||
/**
|
||||
* The data to complete a cart after a payment is captured.
|
||||
*/
|
||||
export type CompleteCartAfterPaymentStepInput = {
|
||||
/**
|
||||
* The ID of the cart to complete.
|
||||
*/
|
||||
cart_id: string
|
||||
}
|
||||
|
||||
export const completeCartAfterPaymentStepId = "complete-cart-after-payment-step"
|
||||
/**
|
||||
* This step completes a cart after a payment is captured.
|
||||
*/
|
||||
export const completeCartAfterPaymentStep = createStep(
|
||||
completeCartAfterPaymentStepId,
|
||||
async (input: CompleteCartAfterPaymentStepInput, { container }) => {
|
||||
const workflowEngine = container.resolve(Modules.WORKFLOW_ENGINE)
|
||||
|
||||
await workflowEngine.run(completeCartWorkflowId, {
|
||||
input: {
|
||||
id: input.cart_id,
|
||||
},
|
||||
transactionId: input.cart_id,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -1,9 +1,9 @@
|
||||
import { WebhookActionResult } from "@medusajs/types"
|
||||
import { PaymentActions } from "@medusajs/utils"
|
||||
import { createWorkflow, when } from "@medusajs/workflows-sdk"
|
||||
import { completeCartWorkflow } from "../../cart/workflows/complete-cart"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { authorizePaymentSessionStep } from "../steps"
|
||||
import { completeCartAfterPaymentStep } from "../steps/complete-cart-after-payment"
|
||||
import { capturePaymentWorkflow } from "./capture-payment"
|
||||
|
||||
/**
|
||||
@@ -131,13 +131,11 @@ export const processPaymentWorkflow = createWorkflow(
|
||||
when({ cartPaymentCollection }, ({ cartPaymentCollection }) => {
|
||||
return !!cartPaymentCollection.data.length
|
||||
}).then(() => {
|
||||
completeCartWorkflow
|
||||
.runAsStep({
|
||||
input: { id: cartPaymentCollection.data[0].cart_id },
|
||||
})
|
||||
.config({
|
||||
continueOnPermanentFailure: true, // Continue payment processing even if cart completion fails
|
||||
})
|
||||
completeCartAfterPaymentStep({
|
||||
cart_id: cartPaymentCollection.data[0].cart_id,
|
||||
}).config({
|
||||
continueOnPermanentFailure: true, // Continue payment processing even if cart completion fails
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user