docs: update recipes and tutorials to support locks and idempotency (#14151)

This commit is contained in:
Shahed Nasser
2025-12-01 09:01:25 +02:00
committed by GitHub
parent bbf294fc31
commit 1e2f40b623
14 changed files with 1176 additions and 396 deletions

View File

@@ -375,36 +375,50 @@ The workflow you'll implement in this section has the following steps:
type: "step",
name: "useQueryGraphStep (Retrieve Variant)",
description: "Retrieve the variant's details using Query",
depth: 1,
depth: 2,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "getVariantMetalPricesStep",
description: "Retrieve the variant's price using the third-party service.",
depth: 1,
depth: 3,
link: "#getvariantmetalpricesstep"
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
depth: 4,
link: "/references/medusa-workflows/steps/acquireLockStep",
},
{
type: "step",
name: "addToCartWorkflow",
description: "Add the item with the custom price to the cart.",
depth: 1,
depth: 5,
link: "/references/medusa-workflows/addToCartWorkflow"
},
{
type: "step",
name: "useQueryGraphStep (Retrieve Cart)",
description: "Retrieve the updated cart's details using Query.",
depth: 1,
depth: 6,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
depth: 7,
link: "/references/medusa-workflows/steps/releaseLockStep",
}
]
}}
hideLegend
/>
`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`.
You'll only implement the `getVariantMetalPricesStep`. Medusa provides the other steps out-of-the-box.
### getVariantMetalPricesStep
@@ -519,7 +533,7 @@ Create the file `src/workflows/add-custom-to-cart.ts` with the following content
export const workflowHighlights = [
["17", "useQueryGraphStep", "Retrieve the cart's details."],
["23", "useQueryGraphStep", "Retrieve the variant's details."],
["26", "useQueryGraphStep", "Retrieve the variant's details."],
]
```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights}
@@ -543,6 +557,9 @@ export const addCustomToCartWorkflow = createWorkflow(
entity: "cart",
filters: { id: cart_id },
fields: ["id", "currency_code"],
options: {
throwIfKeyNotFound: true,
},
})
const { data: variants } = useQueryGraphStep({
@@ -607,7 +624,10 @@ Next, you'll add the item with the custom price to the cart. First, add the foll
```ts title="src/workflows/add-custom-to-cart.ts"
import { transform } from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
import {
acquireLockStep,
addToCartWorkflow,
} from "@medusajs/medusa/core-flows"
```
Then, replace the `TODO` in the workflow with the following:
@@ -623,6 +643,12 @@ const itemToAdd = transform({
}]
})
acquireLockStep({
key: cart_id,
timeout: 2,
ttl: 10,
})
addToCartWorkflow.runAsStep({
input: {
items: itemToAdd,
@@ -633,7 +659,9 @@ addToCartWorkflow.runAsStep({
// TODO retrieve and return cart
```
You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart.
You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow.
After that, you use Medusa's `acquireLockStep` to acquire a lock on the cart, and `addToCartWorkflow` to add the item with the custom price to the cart.
<Note title="Tip">
@@ -645,6 +673,9 @@ Lastly, you'll retrieve the cart's details again and return them. Add the follow
```ts title="src/workflows/add-custom-to-cart.ts"
import { WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import {
releaseLockStep,
} from "@medusajs/medusa/core-flows"
```
And replace the last `TODO` in the workflow with the following:
@@ -656,12 +687,18 @@ const { data: updatedCarts } = useQueryGraphStep({
fields: ["id", "items.*"],
}).config({ name: "refetch-cart" })
releaseLockStep({
key: cart_id,
})
return new WorkflowResponse({
cart: updatedCarts[0],
})
```
In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.
In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. Then, you release the lock on the cart with the `releaseLockStep`.
To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.
In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart.

View File

@@ -1186,12 +1186,26 @@ The workflow has the following steps:
link: "/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow",
depth: 1
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire lock on the cart to avoid race conditions",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2,
},
{
type: "workflow",
name: "addShippingMethodToCartWorkflow",
description: "Add shipping method to the cart",
link: "/references/medusa-workflows/addShippingMethodToCartWorkflow",
depth: 2
depth: 3
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 4,
}
],
depth: 8
@@ -1210,7 +1224,7 @@ These steps and workflows are available in Medusa out-of-the-box. So, you can im
Create the file `src/workflows/create-checkout-session.ts` with the following content:
```ts title="src/workflows/create-checkout-session.ts" collapsibleLines="1-18" expandButtonLabel="Show Imports"
```ts title="src/workflows/create-checkout-session.ts" collapsibleLines="1-20" expandButtonLabel="Show Imports"
import {
createWorkflow,
transform,
@@ -1218,11 +1232,13 @@ import {
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
addShippingMethodToCartWorkflow,
createCartWorkflow,
CreateCartWorkflowInput,
createCustomersWorkflow,
listShippingOptionsForCartWithPricingWorkflow,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
@@ -1463,9 +1479,17 @@ when(input, (input) => !!input.fulfillment_address)
}],
}
})
acquireLockStep({
key: createdCart.id,
timeout: 2,
ttl: 10,
})
addShippingMethodToCartWorkflow.runAsStep({
input: shippingMethodData,
})
releaseLockStep({
key: createdCart.id,
})
})
// TODO prepare checkout session response
@@ -1475,7 +1499,9 @@ You use the `when` function to check if a fulfillment address is provided in the
- Retrieve the shipping options using the [listShippingOptionsForCartWithPricingWorkflow](/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow).
- Create a variable with the cheapest shipping option using the `transform` function.
- Acquire a lock on the cart to avoid race conditions.
- Add the cheapest shipping option to the cart using the [addShippingMethodToCartWorkflow](/references/medusa-workflows/addShippingMethodToCartWorkflow).
- Release the lock on the cart.
#### Prepare Checkout Session Response
@@ -1895,12 +1921,19 @@ The workflow has the following steps:
],
depth: 4
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to avoid concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 5,
},
{
type: "workflow",
name: "updateCartWorkflow",
description: "Update the cart with the new data",
link: "/references/medusa-workflows/updateCartWorkflow",
depth: 5
depth: 6
},
{
type: "when",
@@ -1914,13 +1947,20 @@ The workflow has the following steps:
depth: 1
}
],
depth: 6
depth: 7
},
{
type: "workflow",
name: "prepareCheckoutSessionDataWorkflow",
description: "Prepare the checkout session response",
depth: 7
depth: 8
},
{
type: "step",
name: "releaseLockStep",
description: "Release lock on the cart",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 9
}
]
}}
@@ -1930,7 +1970,7 @@ These steps and workflows are available in Medusa out-of-the-box. So, you can im
Create the file `src/workflows/update-checkout-session.ts` with the following content:
```ts title="src/workflows/update-checkout-session.ts" collapsibleLines="1-16" expandButtonLabel="Show Imports"
```ts title="src/workflows/update-checkout-session.ts" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import {
createWorkflow,
transform,
@@ -1938,8 +1978,10 @@ import {
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
addShippingMethodToCartWorkflow,
createCustomersWorkflow,
releaseLockStep,
updateCartWorkflow,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
@@ -2071,6 +2113,11 @@ You use the `when` function to check if items are provided in the input. If so,
Next, you'll update the cart based on the input. Replace the `TODO` in the workflow with the following:
```ts title="src/workflows/update-checkout-session.ts"
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// Prepare update data
const updateData = transform({
input,
@@ -2106,9 +2153,11 @@ updateCartWorkflow.runAsStep({
// TODO add shipping method if fulfillment option ID is provided
```
You use the `transform` function to prepare the input for the `updateCartWorkflow` workflow. You map the input properties to the cart properties.
First, you acquire a lock on the cart to avoid concurrent modifications.
Then, you update the cart using the `updateCartWorkflow`. This workflow will also clear the cart's payment sessions.
Then, you use the `transform` function to prepare the input for the `updateCartWorkflow` workflow. You map the input properties to the cart properties.
After that, you update the cart using the `updateCartWorkflow`. This workflow will also clear the cart's payment sessions.
#### Add Shipping Method if Fulfillment Option ID is Provided
@@ -2136,12 +2185,18 @@ const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
},
})
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(responseData)
```
You use the `when` function to check if a fulfillment option ID is provided in the input. If it is, you add it to the cart using the `addShippingMethodToCartWorkflow` workflow.
Then, you prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow you created earlier. You return it as the workflow's response.
Then, you prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow you created earlier.
Finally, you release the lock on the cart and return the prepared response as the workflow's response.
### b. Update Checkout Session API Route
@@ -2425,6 +2480,13 @@ The workflow that completes a checkout session has the following steps:
link: "/references/helper-steps/useQueryGraphStep",
depth: 1,
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2,
},
{
type: "when",
condition: "!!input.payment_data.billing_address",
@@ -2437,7 +2499,7 @@ The workflow that completes a checkout session has the following steps:
depth: 1
}
],
depth: 2
depth: 3
},
{
type: "when",
@@ -2471,7 +2533,7 @@ The workflow that completes a checkout session has the following steps:
depth: 4
}
],
depth: 3
depth: 4
},
{
type: "when",
@@ -2484,7 +2546,14 @@ The workflow that completes a checkout session has the following steps:
depth: 1
}
],
depth: 4
depth: 5
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 6,
}
]
}}
@@ -2494,7 +2563,7 @@ These steps and workflows are available in Medusa out-of-the-box. So, you can im
To create the workflow, create the file `src/workflows/complete-checkout-session.ts` with the following content:
```ts title="src/workflows/complete-checkout-session.ts" collapsibleLines="1-19" expandButtonLabel="Show Imports"
```ts title="src/workflows/complete-checkout-session.ts" collapsibleLines="1-21" expandButtonLabel="Show Imports"
import {
createWorkflow,
transform,
@@ -2502,10 +2571,12 @@ import {
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
completeCartWorkflow,
createPaymentCollectionForCartWorkflow,
createPaymentSessionsWorkflow,
refreshPaymentCollectionForCartWorkflow,
releaseLockStep,
updateCartWorkflow,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
@@ -2556,6 +2627,11 @@ export const completeCheckoutSessionWorkflow = createWorkflow(
throwIfKeyNotFound: true,
},
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// TODO update cart with billing address if provided
}
@@ -2564,7 +2640,7 @@ export const completeCheckoutSessionWorkflow = createWorkflow(
The `completeCheckoutSessionWorkflow` accepts an input with the properties received from the AI agent to complete the checkout session.
So far, you retrieve the cart using the `useQueryGraphStep` step.
So far, you retrieve the cart using the `useQueryGraphStep`, and you acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
#### Update Cart with Billing Address
@@ -2744,10 +2820,16 @@ const responseData = transform({
return data.completeCartResponse || data.invalidPaymentResponse
})
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(responseData)
```
You use `transform` to pick either the response from completing the cart or the error response for an invalid payment provider. Then, you return the response.
You use `transform` to pick either the response from completing the cart or the error response for an invalid payment provider.
Then, you release the lock on the cart using the `releaseLockStep` step, and return the response as the workflow's response.
### b. Complete Checkout Session API Route
@@ -3013,6 +3095,13 @@ The workflow that cancels a checkout session has the following steps:
description: "Validate if the cart can be canceled",
depth: 2
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 3,
},
{
type: "when",
condition: "!!data.carts[0].payment_collection?.payment_sessions?.length",
@@ -3024,20 +3113,27 @@ The workflow that cancels a checkout session has the following steps:
depth: 1
}
],
depth: 3
depth: 4
},
{
type: "workflow",
name: "updateCartWorkflow",
description: "Update the cart status to canceled",
link: "/references/medusa-workflows/updateCartWorkflow",
depth: 4
depth: 5
},
{
type: "workflow",
name: "prepareCheckoutSessionDataWorkflow",
description: "Prepare the checkout session response",
depth: 5
depth: 6
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 7
}
]
}}
@@ -3173,7 +3269,7 @@ Create the file `src/workflows/cancel-checkout-session.ts` with the following co
```ts title="src/workflows/cancel-checkout-session.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { validateCartCancelationStep, ValidateCartCancelationStepInput } from "./steps/validate-cart-cancelation"
import { updateCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { acquireLockStep, releaseLockStep, updateCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { cancelPaymentSessionsStep } from "./steps/cancel-payment-sessions"
import { prepareCheckoutSessionDataWorkflow } from "./prepare-checkout-session-data"
@@ -3204,6 +3300,12 @@ export const cancelCheckoutSessionWorkflow = createWorkflow(
cart: carts[0],
} as unknown as ValidateCartCancelationStepInput)
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// TODO cancel payment sessions if any
}
)
@@ -3211,7 +3313,11 @@ export const cancelCheckoutSessionWorkflow = createWorkflow(
The `cancelCheckoutSessionWorkflow` accepts an input with the cart ID of the checkout session to cancel.
So far, you retrieve the cart using the `useQueryGraphStep` step and validate that the cart can be canceled using the `validateCartCancelationStep`.
So far, you:
1. Retrieve the cart using the `useQueryGraphStep` step.
2. Validate that the cart can be canceled using the `validateCartCancelationStep`.
3. Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
Next, you'll cancel the payment sessions if there are any. Replace the `TODO` in the workflow with the following:
@@ -3246,7 +3352,7 @@ You use the `when` function to check if the cart has any payment sessions. If so
You also update the cart using the `updateCartWorkflow` workflow to add a `checkout_session_canceled` metadata field to the cart. This is useful to detect canceled checkout sessions in the future.
Finally, you'll prepare and return the checkout session response. Replace the `TODO` in the workflow with the following:
Finally, you'll prepare the checkout session response, release the lock, and return the response. Replace the `TODO` in the workflow with the following:
```ts title="src/workflows/cancel-checkout-session.ts"
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
@@ -3255,10 +3361,14 @@ const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
},
})
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(responseData)
```
You prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow and return it as the workflow's response.
You prepare the checkout session response using the `prepareCheckoutSessionDataWorkflow` workflow, then you release the lock on the cart using the `releaseLockStep`. Finally, you return the response.
### b. Cancel Checkout Session API Route

View File

@@ -3072,6 +3072,13 @@ The workflow to add a customer's tier promotion to a cart has the following step
link: "/references/helper-steps/useQueryGraphStep",
depth: 1,
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2,
},
{
type: "when",
condition: "!!data.carts[0].customer",
@@ -3083,7 +3090,7 @@ The workflow to add a customer's tier promotion to a cart has the following step
depth: 1,
}
],
depth: 2,
depth: 3,
},
{
type: "when",
@@ -3096,7 +3103,14 @@ The workflow to add a customer's tier promotion to a cart has the following step
depth: 1
}
],
depth: 3,
depth: 4,
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 5,
}
]
}}
@@ -3157,14 +3171,19 @@ You can now create the workflow that adds a customer's tier promotion to a cart.
To create the workflow, create the file `src/workflows/add-tier-promotion-to-cart.ts` with the following content:
```ts title="src/workflows/add-tier-promotion-to-cart.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports"
```ts title="src/workflows/add-tier-promotion-to-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports"
import {
createWorkflow,
WorkflowResponse,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { updateCartPromotionsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import {
acquireLockStep,
releaseLockStep,
updateCartPromotionsWorkflow,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { PromotionActions } from "@medusajs/framework/utils"
import { validateTierPromotionStep } from "./steps/validate-tier-promotion"
@@ -3197,6 +3216,12 @@ export const addTierPromotionToCartWorkflow = createWorkflow(
},
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// Check if customer exists and has tier
const validationResult = when({ carts }, (data) => !!data.carts[0].customer).then(() => {
@@ -3240,6 +3265,10 @@ export const addTierPromotionToCartWorkflow = createWorkflow(
})
})
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(void 0)
}
)
@@ -3250,8 +3279,10 @@ The workflow receives the cart's ID as input.
In the workflow, you:
- Retrieve the cart details using `useQueryGraphStep`.
- Acquire a lock on the cart using `acquireLockStep`.
- Validate that the customer exists and has a tier promotion using `validateTierPromotionStep`.
- Update the cart's promotions if the customer has a tier promotion that hasn't been applied yet, using `updateCartPromotionsWorkflow`.
- Release the lock on the cart using `releaseLockStep`.
### b. Cart Updated Subscriber

View File

@@ -1278,48 +1278,62 @@ The workflow will have the following steps:
type: "step",
name: "validateCustomerExistsStep",
description: "Validate that the customer is registered.",
depth: 1,
depth: 2,
},
{
type: "step",
name: "getCartLoyaltyPromoStep",
description: "Retrieve the cart's loyalty promotion.",
depth: 1,
depth: 3,
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 4,
},
{
type: "step",
name: "getCartLoyaltyPromoAmountStep",
description: "Get the amount to be discounted based on the loyalty points.",
depth: 1,
depth: 5,
},
{
type: "step",
name: "createPromotionsStep",
description: "Create a new loyalty promotion for the cart.",
link: "/references/medusa-workflows/steps/createPromotionsStep",
depth: 1,
depth: 6,
},
{
type: "workflow",
name: "updateCartPromotionsWorkflow",
description: "Update the cart's promotions with the new loyalty promotion.",
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
depth: 1,
depth: 7,
},
{
type: "step",
name: "updateCartsStep",
description: "Update the cart to store the ID of the loyalty promotion in the metadata.",
link: "/references/medusa-workflows/steps/updateCartsStep",
depth: 1,
depth: 8,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the cart's details again.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 1
depth: 9
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 10
}
]
}}
/>
@@ -1399,20 +1413,23 @@ You can now create the workflow that applies a loyalty promotion to the cart.
To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:
export const applyLoyaltyOnCartWorkflowHighlights = [
["44", "useQueryGraphStep", "Retrieve the cart's details."],
["55", "validateCustomerExistsStep", "Validate that the customer is registered."],
["46", "useQueryGraphStep", "Retrieve the cart's details."],
["57", "validateCustomerExistsStep", "Validate that the customer is registered."],
["59", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
["64", "getCartLoyaltyPromoAmountStep", "Get the amount to be discounted based on the loyalty points."],
["61", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["72", "getCartLoyaltyPromoAmountStep", "Get the amount to be discounted based on the loyalty points."],
]
```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-24" expandButtonLabel="Show Imports"
```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={applyLoyaltyOnCartWorkflowHighlights} collapsibleLines="1-26" expandButtonLabel="Show Imports"
import {
createWorkflow,
transform,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
createPromotionsStep,
releaseLockStep,
updateCartPromotionsWorkflow,
updateCartsStep,
useQueryGraphStep,
@@ -1469,6 +1486,12 @@ export const applyLoyaltyOnCartWorkflow = createWorkflow(
throwErrorOn: "found",
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const amount = getCartLoyaltyPromoAmountStep({
cart: carts[0],
} as unknown as GetCartLoyaltyPromoAmountStepInput)
@@ -1485,6 +1508,7 @@ So far, you:
- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Validate that the customer is registered using the `validateCustomerExistsStep`.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `found` to throw an error if a loyalty promotion is found in the cart.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Retrieve the amount to be discounted based on the loyalty points using the `getCartLoyaltyPromoAmountStep`.
Next, you need to create a new loyalty promotion for the cart. First, you'll prepare the data of the promotion to be created.
@@ -1564,6 +1588,7 @@ export const createLoyaltyPromoStepHighlights = [
["25", "updateCartPromotionsWorkflow", "Update the cart's promotions with the new loyalty promotion."],
["29", "updateCartsStep", "Update the cart to store the ID of the loyalty promotion in the metadata."],
["37", "useQueryGraphStep", "Retrieve the cart's details again."],
["43", "releaseLockStep", "Release the lock on the cart."],
]
```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
@@ -1609,6 +1634,10 @@ const { data: updatedCarts } = useQueryGraphStep({
filters: { id: input.cart_id },
}).config({ name: "retrieve-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(updatedCarts[0])
```
@@ -1619,6 +1648,7 @@ In the rest of the workflow, you:
- Update the cart's promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
- Release the lock on the cart using the `releaseLockStep`.
To return data from the workflow, you must return an instance of `WorkflowResponse`. You pass it the data to be returned, which is in this case the cart's details.
@@ -1764,35 +1794,49 @@ The workflow will have the following steps:
type: "step",
name: "getCartLoyaltyPromoStep",
description: "Retrieve the cart's loyalty promotion.",
depth: 1,
depth: 2,
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 3,
},
{
type: "workflow",
name: "updateCartPromotionsWorkflow",
description: "Update the cart's promotions to remove the loyalty promotion.",
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
depth: 1,
depth: 4,
},
{
type: "step",
name: "updateCartsStep",
description: "Update the cart to remove the loyalty promotion ID from the metadata.",
link: "/references/medusa-workflows/steps/updateCartsStep",
depth: 1,
depth: 5,
},
{
type: "step",
name: "updatePromotionsStep",
description: "Deactivate the loyalty promotion.",
link: "/references/medusa-workflows/steps/updatePromotionsStep",
depth: 1,
depth: 6,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the cart's details again.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 1
depth: 7
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 8
}
]
}}
@@ -1803,13 +1847,15 @@ Since you already have all the steps, you can create the workflow.
To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:
export const removeLoyaltyFromCartWorkflowHighlights = [
["35", "useQueryGraphStep", "Retrieve the cart's details."],
["43", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
["48", "updateCartPromotionsWorkflow", "Update the cart's promotions to remove the loyalty promotion."],
["56", "transform", "Prepare the new metadata to remove the loyalty promotion ID."],
["67", "updateCartsStep", "Update the cart to remove the loyalty promotion ID from the metadata."],
["74", "updatePromotionsStep", "Deactivate the loyalty promotion."],
["82", "useQueryGraphStep", "Retrieve the cart's details again."],
["37", "useQueryGraphStep", "Retrieve the cart's details."],
["48", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
["53", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["59", "updateCartPromotionsWorkflow", "Update the cart's promotions to remove the loyalty promotion."],
["67", "transform", "Prepare the new metadata to remove the loyalty promotion ID."],
["78", "updateCartsStep", "Update the cart to remove the loyalty promotion ID from the metadata."],
["85", "updatePromotionsStep", "Deactivate the loyalty promotion."],
["93", "useQueryGraphStep", "Retrieve the cart's details again."],
["99", "releaseLockStep", "Release the lock on the cart."]
]
```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
@@ -1819,6 +1865,8 @@ import {
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
releaseLockStep,
useQueryGraphStep,
updateCartPromotionsWorkflow,
updateCartsStep,
@@ -1853,6 +1901,9 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
filters: {
id: input.cart_id,
},
options: {
throwIfKeyNotFound: true,
},
})
const loyaltyPromo = getCartLoyaltyPromoStep({
@@ -1860,6 +1911,12 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
throwErrorOn: "not-found",
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
updateCartPromotionsWorkflow.runAsStep({
input: {
cart_id: input.cart_id,
@@ -1900,6 +1957,10 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
filters: { id: input.cart_id },
}).config({ name: "retrieve-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse(updatedCarts[0])
}
)
@@ -1911,11 +1972,13 @@ In the workflow, you:
- Use `useQueryGraphStep` to retrieve the cart's details. You pass the cart's ID as a filter to retrieve the cart.
- Check whether the cart has a loyalty promotion using the `getCartLoyaltyPromoStep`. You pass the `throwErrorOn` parameter with the value `not-found` to throw an error if a loyalty promotion isn't found in the cart.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Update the cart's promotions using the `updateCartPromotionsWorkflow`, removing the loyalty promotion.
- Use the `transform` function to prepare the new metadata of the cart. You remove the `loyalty_promo_id` from the metadata.
- Update the cart's metadata with the new metadata using the `updateCartsStep`.
- Deactivate the loyalty promotion using the `updatePromotionsStep`.
- Retrieve the cart's details again using `useQueryGraphStep` to get the updated cart with the new loyalty promotion.
- Release the lock on the cart using the `releaseLockStep`.
- Return the cart's details in a `WorkflowResponse` instance.
### Create the API Route

View File

@@ -1576,29 +1576,43 @@ The workflow that completes a cart with pre-order items has the following steps:
workflow={{
name: "completeCartPreorderWorkflow",
steps: [
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 1,
},
{
type: "workflow",
name: "completeCartWorkflow",
description: "Complete the cart with pre-order items.",
link: "/references/medusa-workflows/completeCartWorkflow",
depth: 1,
depth: 2,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve existing preorders of the order for idempotency.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 3,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve all line items in the cart.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 2,
depth: 4,
},
{
type: "step",
name: "retrievePreorderItemIdsStep",
description: "Retrieve the IDs of pre-order variants in the cart.",
depth: 3,
depth: 5,
},
{
type: "when",
condition: "preorderItemIds.length > 0",
condition: "preorders.length === 0 && preorderItemIds.length > 0",
steps: [
{
type: "step",
@@ -1607,15 +1621,22 @@ The workflow that completes a cart with pre-order items has the following steps:
depth: 1,
}
],
depth: 4
depth: 6
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the created order.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 5,
depth: 7,
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 8,
}
]
}}
/>
@@ -1723,19 +1744,34 @@ You can now create the workflow that completes a cart with pre-order items.
Create the file `src/workflows/complete-cart-preorder.ts` with the following content:
export const completeCartPreorderWorkflowHighlights = [
["13", "completeCartWorkflow", "Complete the cart and place the order."],
["19", "useQueryGraphStep", "Retrieve all line items in the cart."],
["30", "retrievePreorderItemIdsStep", "Retrieve the IDs of pre-order variants in the cart."],
["34", "when", "Check if there are pre-order items in the cart."],
["38", "createPreordersStep", "Create pre-ordersz for the pre-order items in the cart."],
["44", "useQueryGraphStep", "Retrieve the created order."],
["62", "order", "Return the created Medusa order."],
["25", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["30", "completeCartWorkflow", "Complete the cart and place the order."],
["36", "useQueryGraphStep", "Retrieve existing preorders of the order for idempotency."],
["46", "useQueryGraphStep", "Retrieve all line items in the cart."],
["57", "retrievePreorderItemIdsStep", "Retrieve the IDs of pre-order variants in the cart."],
["61", "when", "Check that there are no existing preorders and that there are pre-order items in the cart."],
["66", "createPreordersStep", "Create pre-ordersz for the pre-order items in the cart."],
["72", "useQueryGraphStep", "Retrieve the created order."],
["89", "releaseLockStep", "Release the lock on the cart."],
["94", "order", "Return the created Medusa order."],
]
```ts title="src/workflows/complete-cart-preorder.ts" highlights={completeCartPreorderWorkflowHighlights}
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { completeCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { retrievePreorderItemIdsStep, RetrievePreorderItemIdsStepInput } from "./steps/retrieve-preorder-items"
```ts title="src/workflows/complete-cart-preorder.ts" highlights={completeCartPreorderWorkflowHighlights} collapsibleLines="1-17" expandButtonLabel="Show Imports"
import {
createWorkflow,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
completeCartWorkflow,
useQueryGraphStep,
releaseLockStep,
} from "@medusajs/medusa/core-flows"
import {
retrievePreorderItemIdsStep,
RetrievePreorderItemIdsStepInput,
} from "./steps/retrieve-preorder-items"
import { createPreordersStep } from "./steps/create-preorders"
type WorkflowInput = {
@@ -1745,12 +1781,27 @@ type WorkflowInput = {
export const completeCartPreorderWorkflow = createWorkflow(
"complete-cart-preorder",
(input: WorkflowInput) => {
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const { id } = completeCartWorkflow.runAsStep({
input: {
id: input.cart_id,
},
})
const { data: preorders } = useQueryGraphStep({
entity: "preorder",
fields: [
"id",
],
filters: {
order_id: id,
},
})
const { data: line_items } = useQueryGraphStep({
entity: "line_item",
fields: [
@@ -1760,15 +1811,16 @@ export const completeCartPreorderWorkflow = createWorkflow(
filters: {
cart_id: input.cart_id,
},
})
}).config({ name: "retrieve-line-items" })
const preorderItemIds = retrievePreorderItemIdsStep({
line_items,
} as unknown as RetrievePreorderItemIdsStepInput)
when({
preorders,
preorderItemIds,
}, (data) => data.preorderItemIds.length > 0)
}, (data) => data.preorders.length === 0 && data.preorderItemIds.length > 0)
.then(() => {
createPreordersStep({
preorder_variant_ids: preorderItemIds,
@@ -1793,6 +1845,10 @@ export const completeCartPreorderWorkflow = createWorkflow(
},
}).config({ name: "retrieve-order" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
order: orders[0],
@@ -1805,13 +1861,17 @@ The workflow receives the cart ID as input.
In the workflow, you:
1. Complete the cart using the [completeCartWorkflow](/references/medusa-workflows/completeCartWorkflow) as a step. This is Medusa's cart completion logic.
2. Retrieve all line items in the cart using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
3. Retrieve the IDs of the pre-order variants in the cart using the `retrievePreorderItemIdsStep`.
4. Use [when-then](!docs!/learn/fundamentals/workflows/conditions) to check if there are pre-order items in the cart.
- If so, you create `Preorder` records for the pre-order items using the `createPreordersStep`.
5. Retrieve the created order using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
6. Return the created Medusa order.
1. Acquire a lock on the cart using the [acquireLockStep](/references/medusa-workflows/steps/acquireLockStep) to prevent concurrent modifications.
2. Complete the cart using the [completeCartWorkflow](/references/medusa-workflows/completeCartWorkflow) as a step. This is Medusa's cart completion logic.
3. Retrieve existing pre-orders of the created order using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
- This is essential for idempotency, ensuring that pre-orders are not created multiple times if the workflow is retried.
4. Retrieve all line items in the cart using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
5. Retrieve the IDs of the pre-order variants in the cart using the `retrievePreorderItemIdsStep`.
6. Use [when-then](!docs!/learn/fundamentals/workflows/conditions) to check if there are no existing pre-orders and if there are pre-order items in the cart.
- If the condition is met, you create `Preorder` records for the pre-order items using the `createPreordersStep`.
7. Retrieve the created order using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
8. Release the lock on the cart using the [releaseLockStep](/references/medusa-workflows/steps/releaseLockStep).
9. Return the created Medusa order.
### b. Create Complete Pre-order Cart API Route

View File

@@ -4322,18 +4322,25 @@ The workflow will have the following steps:
description: "Validates the product builder configuration",
depth: 1,
},
{
type: "step",
name: "acquireLockStep",
description: "Acquires a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2,
},
{
type: "step",
name: "addToCartWorkflow",
description: "Adds the product to the cart.",
depth: 2
depth: 3
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get cart with items details.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 3
depth: 4
},
{
type: "when",
@@ -4353,14 +4360,21 @@ The workflow will have the following steps:
depth: 2
},
],
depth: 4
depth: 5
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get updated cart details.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 5
depth: 6
},
{
type: "step",
name: "releaseLockStep",
description: "Releases the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 7
}
]
}}
@@ -4531,18 +4545,27 @@ You can now implement the workflow that adds products with builder configuration
Create the file `src/workflows/add-product-builder-to-cart.ts` with the following content:
export const addToCartWorkflowHighlights = [
["24", "validateProductBuilderConfigurationStep", "Validate user selections."],
["32", "validateProductBuilderConfigurationStep", "Validate user selections."],
["39", "acquireLockStep", "Acquire lock on cart."]
]
```ts title="src/workflows/add-product-builder-to-cart.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports" highlights={addToCartWorkflowHighlights}
```ts title="src/workflows/add-product-builder-to-cart.ts" collapsibleLines="1-17" expandButtonLabel="Show Imports" highlights={addToCartWorkflowHighlights}
import {
createWorkflow,
WorkflowResponse,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { validateProductBuilderConfigurationStep } from "./steps/validate-product-builder-configuration"
import {
addToCartWorkflow,
updateLineItemInCartWorkflow,
useQueryGraphStep,
acquireLockStep,
releaseLockStep,
} from "@medusajs/medusa/core-flows"
import {
validateProductBuilderConfigurationStep,
} from "./steps/validate-product-builder-configuration"
type AddProductBuilderToCartInput = {
cart_id: string
@@ -4565,6 +4588,12 @@ export const addProductBuilderToCartWorkflow = createWorkflow(
addon_variants: input.addon_variants,
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// TODO add main, complementary, and addon product variants to the cart
}
)
@@ -4572,7 +4601,7 @@ export const addProductBuilderToCartWorkflow = createWorkflow(
The workflow accepts the cart, product, variant, and builder configuration information as input.
So far, you only validate the product builder configuration using the step you created earlier. If the validation fails, the workflow will stop executing.
So far, you validate the product builder configuration using the step you created earlier. If the validation fails, the workflow will stop executing. You also acquire a lock on the cart to prevent concurrent modifications.
Next, you need to add the main product variant to the cart. Replace the `TODO` with the following:
@@ -4749,12 +4778,16 @@ const { data: updatedCart } = useQueryGraphStep({
},
}).config({ name: "get-final-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
cart: updatedCart[0],
})
```
You retrieve the final cart details after all items have been added, and you return the updated cart.
You retrieve the final cart details after all items have been added. Then, you release the lock on the cart and return the updated cart in the workflow response.
### b. Create API Route
@@ -5713,20 +5746,34 @@ The workflow to remove a product with builder configurations from the cart has t
link: "/references/helper-steps/useQueryGraphStep",
depth: 1
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2,
},
{
type: "workflow",
name: "deleteLineItemsWorkflow",
description: "Delete line items from the cart.",
link: "/references/medusa-workflows/deleteLineItemsWorkflow",
depth: 2,
depth: 3,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the updated cart details.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 3
depth: 4
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 5,
}
]
}}
hideLegend
@@ -5737,20 +5784,27 @@ Medusa provides all of these steps, so you can create the workflow without needi
Create the file `src/workflows/remove-product-builder-from-cart.ts` with the following content:
export const removeProductBuilderFromCartWorkflowHighlights = [
["17", "carts", "Retrieve cart."],
["29", "itemsToRemove", "Identify items to remove."],
["43", "relatedItems", "Identify addon items to remove."],
["60", "deleteLineItemsWorkflow", "Delete line items from cart."],
["65", "updatedCart", "Retrieve updated cart."]
["22", "carts", "Retrieve cart."],
["33", "acquireLockStep", "Acquire lock on cart."],
["40", "itemsToRemove", "Identify items to remove."],
["51", "relatedItems", "Identify addon items to remove."],
["68", "deleteLineItemsWorkflow", "Delete line items from cart."],
["75", "updatedCart", "Retrieve updated cart."],
["84", "releaseLockStep", "Release lock on cart."],
]
```ts title="src/workflows/remove-product-builder-from-cart.ts" highlights={removeProductBuilderFromCartWorkflowHighlights}
```ts title="src/workflows/remove-product-builder-from-cart.ts" highlights={removeProductBuilderFromCartWorkflowHighlights} collapsibleLines="1-12" expandButtonLabel="Show Imports"
import {
createWorkflow,
WorkflowResponse,
transform,
} from "@medusajs/framework/workflows-sdk"
import { deleteLineItemsWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import {
deleteLineItemsWorkflow,
useQueryGraphStep,
acquireLockStep,
releaseLockStep,
} from "@medusajs/medusa/core-flows"
type RemoveProductBuilderFromCartInput = {
cart_id: string
@@ -5763,7 +5817,7 @@ export const removeProductBuilderFromCartWorkflow = createWorkflow(
// Step 1: Get current cart with all items
const { data: carts } = useQueryGraphStep({
entity: "cart",
fields: ["*", "items.*", "items.metadata"],
fields: ["*", "items.*"],
filters: {
id: input.cart_id,
},
@@ -5772,18 +5826,21 @@ export const removeProductBuilderFromCartWorkflow = createWorkflow(
},
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
// Step 2: Remove line item and its addons
const itemsToRemove = transform({
input,
carts,
currentCart: carts,
}, (data) => {
const cart = data.carts[0]
const targetLineItem = cart.items.find(
(item: any) => item.id === data.input.line_item_id
)
const cart = data.currentCart[0]
const targetLineItem = cart.items.find((item: any) => item.id === data.input.line_item_id)
const lineItemIdsToRemove = [data.input.line_item_id]
const isBuilderItem =
targetLineItem?.metadata?.is_builder_main_product === true
const isBuilderItem = targetLineItem?.metadata?.is_builder_main_product === true
if (targetLineItem && isBuilderItem) {
// Find all related addon items
@@ -5820,6 +5877,10 @@ export const removeProductBuilderFromCartWorkflow = createWorkflow(
},
}).config({ name: "get-updated-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
cart: updatedCart[0],
})
@@ -5831,10 +5892,12 @@ This workflow receives the IDs of the cart and the line item to remove.
In the workflow, you:
- Acquire a lock on the cart to prevent concurrent modifications.
- Retrieve the cart details with its items.
- Prepare the line items to remove by identifying the main product and its related addons.
- Remove the line items from the cart.
- Retrieve the updated cart details.
- Release the lock on the cart.
You return the cart details in the response.

View File

@@ -2242,19 +2242,33 @@ The workflow will have the following steps:
],
depth: 3
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 4,
},
{
type: "workflow",
name: "addToCartWorkflow",
description: "Add the product to the cart.",
link: "/references/medusa-workflows/addToCartWorkflow",
depth: 4,
depth: 5,
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve updated cart details.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 5
depth: 6
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 7
}
]
}}
@@ -2472,16 +2486,24 @@ You can now create the `addToCartWithRentalWorkflow` that uses the `validateRent
Create the file `src/workflows/add-to-cart-with-rental.ts` with the following content:
```ts title="src/workflows/add-to-cart-with-rental.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-10" expandButtonLabel="Show Imports"
```ts title="src/workflows/add-to-cart-with-rental.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-18" expandButtonLabel="Show Imports"
import {
createWorkflow,
WorkflowResponse,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import {
acquireLockStep,
addToCartWorkflow,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { QueryContext } from "@medusajs/framework/utils"
import { ValidateRentalCartItemInput, validateRentalCartItemStep } from "./steps/validate-rental-cart-item"
import {
ValidateRentalCartItemInput,
validateRentalCartItemStep,
} from "./steps/validate-rental-cart-item"
type AddToCartWorkflowInput = {
cart_id: string
@@ -2500,7 +2522,7 @@ export const addToCartWithRentalWorkflow = createWorkflow(
options: {
throwIfKeyNotFound: true,
},
}).config({ name: "retrieve-cart" })
})
const { data: variants } = useQueryGraphStep({
entity: "product_variant",
@@ -2536,6 +2558,12 @@ export const addToCartWithRentalWorkflow = createWorkflow(
} as unknown as ValidateRentalCartItemInput)
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const itemToAdd = transform({
input,
rentalData,
@@ -2574,6 +2602,10 @@ export const addToCartWithRentalWorkflow = createWorkflow(
},
}).config({ name: "refetch-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
cart: updatedCart[0],
})
@@ -2588,11 +2620,15 @@ In the workflow, you:
1. Retrieve the cart details using the `useQueryGraphStep`.
2. Retrieve the product variant details using the `useQueryGraphStep`.
3. If the product is rentable, call the `validateRentalCartItemStep` to validate and retrieve rental data.
4. Prepare the item to add to the cart.
4. Acquire a lock on the cart using the `acquireLockStep`.
5. Prepare the item to add to the cart.
- If it's a rentable product, you set the `unit_price` to the calculated rental price.
- For non-rentable products, you don't specify the `unit_price`; Medusa will use the variant's price.
5. Add the item to the cart using the existing `addToCartWorkflow`.
6. Retrieve the updated cart details and return them in the workflow response.
6. Add the item to the cart using the existing `addToCartWorkflow`.
7. Retrieve the updated cart details.
8. Release the lock on the cart using the `releaseLockStep`.
Finally, you return the updated cart in the workflow response.
### b. Add to Cart with Rental API Route
@@ -2817,31 +2853,45 @@ The workflow will have the following steps:
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 2
},
{
type: "step",
name: "validateRentalStep",
description: "Validate rental items in the cart.",
depth: 3
},
{
type: "workflow",
name: "completeCartWorkflow",
description: "Complete the cart and create the order.",
link: "/references/medusa-workflows/completeCartWorkflow",
depth: 4
depth: 3
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve order details.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 5
depth: 4
},
{
type: "step",
name: "createRentalsForOrderStep",
description: "Create rental records for rental items in the order.",
depth: 6
name: "useQueryGraphStep",
description: "Retrieve existing rentals for the order to ensure idempotency.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 5
},
{
type: "when",
condition: "rentals.length === 0 && rentalItems.length > 0",
steps: [
{
type: "step",
name: "validateRentalStep",
description: "Validate rental items in the cart.",
depth: 3
},
{
type: "step",
name: "createRentalsForOrderStep",
description: "Create rental records for rental items in the order.",
depth: 6
},
],
depth: 6,
},
{
type: "step",
@@ -2871,6 +2921,7 @@ import { InferTypeOf } from "@medusajs/framework/types"
import { RentalConfiguration } from "../../modules/rental/models/rental-configuration"
import hasCartOverlap from "../../utils/has-cart-overlap"
import validateRentalDates from "../../utils/validate-rental-dates"
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
export type ValidateRentalInput = {
rental_items: {
@@ -2881,12 +2932,13 @@ export type ValidateRentalInput = {
rental_start_date: Date
rental_end_date: Date
rental_days: number
order_id: string
}[]
}
export const validateRentalStep = createStep(
"validate-rental",
async ({ rental_items }: ValidateRentalInput, { container }) => {
async ({ rental_items, order_id }: ValidateRentalInput, { container }) => {
const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)
for (let i = 0; i < rental_items.length; i++) {
@@ -2971,7 +3023,18 @@ export const validateRentalStep = createStep(
}
}
return new StepResponse({ validated: true })
return new StepResponse({ validated: true, order_id })
},
async (order_id, { container, context }) => {
if (!order_id) {return}
cancelOrderWorkflow(container).run({
input: {
order_id,
},
context,
container,
})
}
)
```
@@ -2986,6 +3049,8 @@ You also check for overlaps between rental items in the cart and existing rental
If any validation fails, you throw an appropriate error. If all validations pass, you return a `StepResponse` indicating success.
You also provide a compensation function that cancels the order if the validation fails after the order has been created.
#### createRentalsForOrderStep
The `createRentalsForOrderStep` creates rental records for rental items in the order after it has been created.
@@ -3067,17 +3132,18 @@ You can now create the `createRentalsWorkflow` that uses the above steps.
Create the file `src/workflows/create-rentals.ts` with the following content:
```ts title="src/workflows/create-rentals.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-18" expandButtonLabel="Show Imports"
```ts title="src/workflows/create-rentals.ts" badgeLabel="Medusa Application" badgeColor="green" collapsibleLines="1-21" expandButtonLabel="Show Imports"
import {
createWorkflow,
WorkflowResponse,
transform,
when,
} from "@medusajs/framework/workflows-sdk"
import {
completeCartWorkflow,
useQueryGraphStep,
acquireLockStep,
releaseLockStep,
completeCartWorkflow,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
ValidateRentalInput,
@@ -3139,18 +3205,12 @@ export const createRentalsWorkflow = createWorkflow(
return rentalItemsList
})
const lockKey = transform({
cart_id,
}, (data) => `cart_rentals_creation_${data.cart_id}`)
acquireLockStep({
key: lockKey,
key: cart_id,
timeout: 2,
ttl: 10,
})
validateRentalStep({
rental_items: rentalItems,
} as unknown as ValidateRentalInput)
const order = completeCartWorkflow.runAsStep({
input: { id: cart_id },
})
@@ -3169,12 +3229,30 @@ export const createRentalsWorkflow = createWorkflow(
options: { throwIfKeyNotFound: true },
}).config({ name: "retrieve-order" })
createRentalsForOrderStep({
order: orders[0],
} as unknown as CreateRentalsForOrderInput)
const { data: rentals } = useQueryGraphStep({
entity: "rental",
fields: [
"id",
],
filters: { order_id: order.id },
}).config({ name: "retrieve-rentals" })
when(
{ rentals, rentalItems },
(data) => data.rentals.length === 0 && data.rentalItems.length > 0
)
.then(() => {
validateRentalStep({
rental_items: rentalItems,
order_id: order.id,
} as unknown as ValidateRentalInput)
createRentalsForOrderStep({
order: orders[0],
} as unknown as CreateRentalsForOrderInput)
})
releaseLockStep({
key: lockKey,
key: cart_id,
})
// @ts-ignore
@@ -3192,10 +3270,13 @@ In the workflow, you:
1. Retrieve the cart details using the `useQueryGraphStep`.
2. Extract the rental items from the cart.
3. Acquire a lock on the cart to prevent race conditions.
4. Validate the rental items using the `validateRentalStep`.
5. Complete the cart and create the order using the existing `completeCartWorkflow`.
6. Retrieve the created order details using the `useQueryGraphStep`.
7. Create rental records for the rental items in the order using the `createRentalsForOrderStep`.
4. Complete the cart and create the order using the existing `completeCartWorkflow`.
5. Retrieve the created order details using the `useQueryGraphStep`.
6. Retrieve existing rentals for the order to ensure idempotency.
- This is essential to avoid creating duplicate rentals if the workflow is retried.
7. Perform a condition with `when` to check that there are no existing rentals for the order and there are rental items in the cart. If the condition is met, you:
1. Validate the rental items in the cart using the `validateRentalStep`.
2. Create rental records for the rental items in the order using the `createRentalsForOrderStep`.
8. Release the lock on the cart.
9. Return the created order in the workflow response.

View File

@@ -1752,21 +1752,35 @@ The add-to-cart workflow for bundled products has the following steps:
name: "prepareBundleCartDataStep",
type: "step",
description: "Validate and prepare the items to be added to the cart.",
depth: 1,
depth: 2,
},
{
name: "acquireLockStep",
type: "step",
description: "Acquire a lock on the cart to prevent concurrent modifications",
depth: 3,
link: "/references/medusa-workflows/steps/acquireLockStep"
},
{
name: "addToCartWorkflow",
type: "step",
description: "Add the items in the bundle to the cart.",
depth: 1,
depth: 4,
link: "/references/medusa-workflows/addToCartWorkflow"
},
{
name: "useQueryGraphStep",
type: "step",
description: "Retrieve the details of the cart.",
depth: 1,
depth: 5,
link: "/references/helper-steps/useQueryGraphStep"
},
{
name: "releaseLockStep",
type: "step",
description: "Release the lock on the cart.",
depth: 6,
link: "/references/medusa-workflows/steps/releaseLockStep"
}
]
}}
@@ -1889,20 +1903,24 @@ You can now create the workflow with the custom add-to-cart logic.
To create the workflow, create the file `src/workflows/add-bundle-to-cart.ts` with the following content:
export const addBundleToCartWorkflowHighlights = [
["28", "useQueryGraphStep", "Retrieve the bundle, its items, and their products and variants."],
["44", "prepareBundleCartDataStep", "Validate and prepare the items to be added to the cart."],
["50", "addToCartWorkflow", "Add the items in the bundle to the cart."],
["57", "useQueryGraphStep", "Retrieve the updated cart."],
["30", "useQueryGraphStep", "Retrieve the bundle, its items, and their products and variants."],
["46", "prepareBundleCartDataStep", "Validate and prepare the items to be added to the cart."],
["52", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["58", "addToCartWorkflow", "Add the items in the bundle to the cart."],
["65", "useQueryGraphStep", "Retrieve the updated cart."],
["71", "releaseLockStep", "Release the lock on the cart."],
]
```ts title="src/workflows/add-bundle-to-cart.ts" highlights={addBundleToCartWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
```ts title="src/workflows/add-bundle-to-cart.ts" highlights={addBundleToCartWorkflowHighlights} collapsibleLines="1-16" expandButtonLabel="Show Imports"
import {
createWorkflow,
transform,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
addToCartWorkflow,
acquireLockStep,
addToCartWorkflow,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import {
@@ -1945,6 +1963,12 @@ export const addBundleToCartWorkflow = createWorkflow(
items,
} as unknown as PrepareBundleCartDataStepInput)
acquireLockStep({
key: cart_id,
timeout: 2,
ttl: 10,
})
addToCartWorkflow.runAsStep({
input: {
cart_id,
@@ -1958,6 +1982,10 @@ export const addBundleToCartWorkflow = createWorkflow(
fields: ["id", "items.*"],
}).config({ name: "refetch-cart" })
releaseLockStep({
key: cart_id,
})
return new WorkflowResponse(updatedCarts[0])
}
)
@@ -1969,8 +1997,10 @@ In the workflow, you:
- Retrieve the bundle, its items, and their products and variants using the `useQueryGraphStep`.
- Validate and prepare the items to be added to the cart using the `prepareBundleCartDataStep`.
- Acquire a lock on the cart using the `acquireLockStep`.
- Add the items to the cart using the `addToCartWorkflow`.
- Retrieve the updated cart using the `useQueryGraphStep`.
- Release the lock on the cart using the `releaseLockStep`.
Finally, you return the updated cart.
@@ -2774,20 +2804,34 @@ The workflow has the following steps:
depth: 1,
link: "/references/helper-steps/useQueryGraphStep"
},
{
name: "acquireLockStep",
type: "step",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
depth: 2,
link: "/references/medusa-workflows/steps/acquireLockStep"
},
{
name: "deleteLineItemsWorkflow",
type: "workflow",
description: "Remove the items in the bundle from the cart.",
depth: 1,
depth: 3,
link: "/references/medusa-workflows/deleteLineItemsWorkflow"
},
{
name: "useQueryGraphStep",
type: "step",
description: "Retrieve the updated cart.",
depth: 1,
depth: 4,
link: "/references/helper-steps/useQueryGraphStep"
}
},
{
name: "releaseLockStep",
type: "step",
description: "Release the lock on the cart.",
depth: 5,
link: "/references/medusa-workflows/steps/releaseLockStep"
},
]
}}
hideLegend
@@ -2798,20 +2842,24 @@ Medusa provides all these steps and workflows in its `@medusajs/medusa/core-flow
Create the file `src/workflows/remove-bundle-from-cart.ts` with the following content:
export const removeBundleFromCartWorkflowHighlights = [
["19", "useQueryGraphStep", "Retrieve the details of the cart and its items."],
["33", "transform", "Prepare the IDs of the items to remove."],
["42", "deleteLineItemsWorkflow", "Remove the items in the bundle from the cart."],
["50", "useQueryGraphStep", "Retrieve the updated cart."],
["21", "useQueryGraphStep", "Retrieve the details of the cart and its items."],
["35", "transform", "Prepare the IDs of the items to remove."],
["44", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["50", "deleteLineItemsWorkflow", "Remove the items in the bundle from the cart."],
["58", "useQueryGraphStep", "Retrieve the updated cart."],
["69", "releaseLockStep", "Release the lock on the cart."],
]
```ts title="src/workflows/remove-bundle-from-cart.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={removeBundleFromCartWorkflowHighlights}
```ts title="src/workflows/remove-bundle-from-cart.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports" highlights={removeBundleFromCartWorkflowHighlights}
import {
createWorkflow,
transform,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
acquireLockStep,
deleteLineItemsWorkflow,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
@@ -2846,6 +2894,12 @@ export const removeBundleFromCartWorkflow = createWorkflow(
}).map((item) => item!.id)
})
acquireLockStep({
key: cart_id,
timeout: 2,
ttl: 10,
})
deleteLineItemsWorkflow.runAsStep({
input: {
cart_id,
@@ -2865,6 +2919,10 @@ export const removeBundleFromCartWorkflow = createWorkflow(
},
}).config({ name: "retrieve-cart" })
releaseLockStep({
key: cart_id,
})
return new WorkflowResponse(updatedCarts[0])
}
)
@@ -2876,8 +2934,10 @@ In the workflow, you:
- Retrieve the cart and its items using the `useQueryGraphStep`.
- Use `transform` to filter the items in the cart and return only the IDs of the items that belong to the bundle.
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
- Remove the items from the cart using the `deleteLineItemsWorkflow`.
- Retrieve the updated cart using the `useQueryGraphStep`.
- Release the lock on the cart using the `releaseLockStep`.
Finally, you return the updated cart.

View File

@@ -1983,6 +1983,8 @@ import {
createRemoteLinkStep,
createOrderFulfillmentWorkflow,
emitEventStep,
acquireLockStep,
releaseLockStep,
} from "@medusajs/medusa/core-flows"
import {
Modules,
@@ -1991,6 +1993,7 @@ import createDigitalProductOrderStep, {
CreateDigitalProductOrderStepInput,
} from "./steps/create-digital-product-order"
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
import digitalProductOrderOrderLink from "../../links/digital-product-order"
type WorkflowInput = {
cart_id: string
@@ -1999,6 +2002,11 @@ type WorkflowInput = {
const createDigitalProductOrderWorkflow = createWorkflow(
"create-digital-product-order",
(input: WorkflowInput) => {
acquireLockStep({
key: input.cart_id,
timeout: 30,
ttl: 120,
})
const { id } = completeCartWorkflow.runAsStep({
input: {
id: input.cart_id,
@@ -2022,19 +2030,31 @@ const createDigitalProductOrderWorkflow = createWorkflow(
},
})
const itemsWithDigitalProducts = transform({
orders,
},
(data) => {
return data.orders[0].items?.filter((item) => item?.variant?.digital_product !== undefined)
const { data: existingLinks } = useQueryGraphStep({
entity: digitalProductOrderOrderLink.entryPoint,
fields: ["digital_product_order.id"],
filters: { order_id: id },
}).config({ name: "retrieve-existing-links" })
const itemsWithDigitalProducts = transform(
{
orders,
},
(data) => {
return data.orders[0].items?.filter(
(item) => item?.variant?.digital_product !== undefined
)
}
)
const digital_product_order = when(
"create-digital-product-order-condition",
itemsWithDigitalProducts,
(itemsWithDigitalProducts) => {
return !!itemsWithDigitalProducts?.length
"create-digital-product-order-condition",
{ itemsWithDigitalProducts, existingLinks },
(data) => {
return (
!!data.itemsWithDigitalProducts?.length &&
data.existingLinks.length === 0
)
}
).then(() => {
const {
@@ -2076,6 +2096,10 @@ const createDigitalProductOrderWorkflow = createWorkflow(
return digital_product_order
})
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
order: orders[0],
digital_product_order,
@@ -2088,13 +2112,18 @@ export default createDigitalProductOrderWorkflow
This creates the workflow `createDigitalProductOrderWorkflow`. It runs the following steps:
1. `completeCartWorkflow` as a step to create the Medusa order.
2. `useQueryGraphStep` to retrieve the orders items with their associated variants and linked digital products.
3. Use `when` to check whether the order has digital products. If so:
1. `acquireLockStep` to acquire a lock on the cart ID.
2. `completeCartWorkflow` as a step to create the Medusa order.
3. `useQueryGraphStep` to retrieve the orders items with their associated variants and linked digital products.
4. `useQueryGraphStep` to retrieve any existing links between the digital product order and the Medusa order.
- This is necessary to ensure the workflow is idempotent, meaning that if the workflow is executed multiple times for the same cart, it doesnt create multiple digital product orders.
5. Use `transform` to filter the orders items to only those having digital products.
6. Use `when` to check whether the order has digital products and no existing links. If so:
1. Use the `createDigitalProductOrderStep` to create the digital product order.
2. Use the `createRemoteLinkStep` to link the digital product order to the Medusa order.
3. Use the `createOrderFulfillmentWorkflow` to create a fulfillment for the digital products in the order.
4. Use the `emitEventStep` to emit a custom event.
7. `releaseLockStep` to release the lock on the cart ID.
The workflow returns the Medusa order and the digital product order, if created.

View File

@@ -1281,47 +1281,75 @@ In this step, youll create a workflow thats executed when the customer pla
depth: 1,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to avoid concurrent modifications.",
depth: 2,
link: "/references/medusa-workflows/steps/acquireLockStep"
},
{
type: "step",
name: "completeCartWorkflow",
description: "Create the parent order from the cart.",
depth: 1,
depth: 3,
link: "/references/medusa-workflows/completeCartWorkflow"
},
{
type: "step",
name: "groupVendorItemsStep",
description: "Group the items by their vendor.",
depth: 1,
link: "#groupvendoritemssstep"
name: "useQueryGraphStep",
description: "Retrieve existing links between the order and variant to ensure idempotency.",
depth: 4,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
type: "workflow",
name: "getOrderDetailWorkflow",
description: "Retrieve the parent order's details.",
depth: 1,
depth: 5,
link: "/references/medusa-workflows/getOrderDetailWorkflow"
},
{
type: "step",
name: "createVendorOrdersStep",
description: "Create child orders for each vendor.",
depth: 1,
link: "#createvendorordersstep"
type: "when",
condition: "existingLinks.length === 0",
steps: [
{
type: "step",
name: "groupVendorItemsStep",
description: "Group the items by their vendor.",
depth: 4,
link: "#groupvendoritemssstep"
},
{
type: "step",
name: "createVendorOrdersStep",
description: "Create child orders for each vendor.",
depth: 6,
link: "#createvendorordersstep"
},
{
type: "step",
name: "createRemoteLinkStep",
description: "Create links between vendors and orders.",
depth: 7,
link: "/references/helper-steps/createRemoteLinkStep"
},
],
depth: 6
},
{
type: "step",
name: "createRemoteLinkStep",
description: "Create links between vendors and orders.",
depth: 1,
link: "/references/helper-steps/createRemoteLinkStep"
}
name: "releaseLockStep",
description: "Release the lock on the cart.",
depth: 7,
link: "/references/medusa-workflows/steps/releaseLockStep"
},
]
}}
hideLegend
/>
You only need to implement the third and fourth steps, as Medusa provides the rest of the steps in its `@medusajs/medusa/core-flows` package.
You only need to implement the `groupVendorItemsStep` and `createVendorOrdersStep` steps, as Medusa provides the rest of the steps in its `@medusajs/medusa/core-flows` package.
### groupVendorItemsStep
@@ -1647,17 +1675,22 @@ Now that you have all the necessary steps, you can create the workflow.
Create the workflow at the file `src/workflows/marketplace/create-vendor-orders/index.ts`:
export const createVendorOrdersWorkflowHighlights = [
["21", "useQueryGraphStep", "Retrieve the cart's details."],
["30", "completeCartWorkflow", "Create the parent order from the cart."],
["36", "groupVendorItemsStep", "Group the items by their vendor."],
["40", "getOrderDetailWorkflow", "Retrieve the parent order's details."],
["61", "createVendorOrdersStep", "Create child orders for each vendor"],
["66", "createRemoteLinkStep", "Create the links returned by the previous step."]
["25", "useQueryGraphStep", "Retrieve the cart's details."],
["34", "acquireLockStep", "Acquire a lock on the cart to avoid concurrent modifications."],
["40", "completeCartWorkflow", "Create the parent order from the cart."],
["46", "useQueryGraphStep", "Retrieve existing links between the order and variant to ensure idempotency."],
["52", "getOrderDetailWorkflow", "Retrieve the parent order's details."],
["70", "when", "Check if there are existing links to ensure idempotency."],
["76", "groupVendorItemsStep", "Group the items by their vendor."],
["83", "createVendorOrdersStep", "Create child orders for each vendor"],
["88", "createRemoteLinkStep", "Create the links returned by the previous step."],
["93", "releaseLockStep", "Release the lock on the cart."],
]
```ts title="src/workflows/marketplace/create-vendor-orders/index.ts" collapsibleLines="1-13" expandMoreLabel="Show Imports"
```ts title="src/workflows/marketplace/create-vendor-orders/index.ts" collapsibleLines="1-17" expandMoreLabel="Show Imports"
import {
createWorkflow,
when,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
import {
@@ -1665,9 +1698,12 @@ import {
createRemoteLinkStep,
completeCartWorkflow,
getOrderDetailWorkflow,
acquireLockStep,
releaseLockStep,
} from "@medusajs/medusa/core-flows"
import groupVendorItemsStep from "./steps/group-vendor-items"
import groupVendorItemsStep, { GroupVendorItemsStepInput } from "./steps/group-vendor-items"
import createVendorOrdersStep from "./steps/create-vendor-orders"
import vendorOrderLink from "../../../links/vendor-order"
type WorkflowInput = {
cart_id: string
@@ -1685,16 +1721,24 @@ const createVendorOrdersWorkflow = createWorkflow(
},
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const { id: orderId } = completeCartWorkflow.runAsStep({
input: {
id: carts[0].id,
},
})
const { vendorsItems } = groupVendorItemsStep({
cart: carts[0],
} as unknown as GroupVendorItemsStepInput)
const { data: existingLinks } = useQueryGraphStep({
entity: vendorOrderLink.entryPoint,
fields: ["vendor.id"],
filters: { order_id: orderId },
}).config({ name: "retrieve-existing-links" })
const order = getOrderDetailWorkflow.runAsStep({
input: {
order_id: orderId,
@@ -1713,19 +1757,36 @@ const createVendorOrdersWorkflow = createWorkflow(
},
})
const {
orders: vendorOrders,
linkDefs,
} = createVendorOrdersStep({
parentOrder: order,
vendorsItems,
const vendorOrders = when(
"create-vendor-order-links",
{ existingLinks },
(data) => data.existingLinks.length === 0
).then(() => {
const { vendorsItems } = groupVendorItemsStep({
cart: carts[0],
} as unknown as GroupVendorItemsStepInput)
const {
orders: vendorOrders,
linkDefs,
} = createVendorOrdersStep({
parentOrder: order,
vendorsItems,
})
createRemoteLinkStep(linkDefs)
return vendorOrders
})
createRemoteLinkStep(linkDefs)
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
parent_order: order,
vendor_orders: vendorOrders,
order,
vendorOrders,
})
}
)
@@ -1736,11 +1797,16 @@ export default createVendorOrdersWorkflow
The workflow receives the cart's ID as an input. In the workflow, you run the following steps:
1. `useQueryGraphStep` to retrieve the cart's details.
2. `completeCartWorkflow` to complete the cart and create a parent order.
3. `groupVendorItemsStep` to group the order's items by their vendor.
4. `getOrderDetailWorkflow` to retrieve the parent order's details.
5. `createVendorOrdersStep` to create child orders for each vendor's items.
6. `createRemoteLinkStep` to create the links returned by the previous step.
2. `acquireLockStep` to acquire a lock on the cart.
3. `completeCartWorkflow` to complete the cart and create a parent order.
4. `useQueryGraphStep` to retrieve existing links between the order and variant to ensure idempotency.
- This is essential, as the workflow might be executed multiple times for the same cart. So, if links already exist, you skip creating vendor orders again.
5. `getOrderDetailWorkflow` to retrieve the parent order's details.
6. Perform a condition with `when` to check if there are existing links. If not, you run the following steps:
- `groupVendorItemsStep` to group the items by their vendor.
- `createVendorOrdersStep` to create child orders for each vendor.
- `createRemoteLinkStep` to create the links returned by the previous step.
7. `releaseLockStep` to release the lock on the cart.
You return the parent and vendor orders.

View File

@@ -744,19 +744,33 @@ The custom add-to-cart workflow will have the following steps:
description: "Get the custom price for the product variant.",
depth: 2
},
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications.",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 3
},
{
type: "workflow",
name: "addToCartWorkflow",
description: "Add the product variant to the cart with the custom price.",
link: "/references/medusa-workflows/addToCartWorkflow",
depth: 3
depth: 4
},
{
type: "step",
name: "useQueryGraphStep",
description: "Get the cart's updated data.",
link: "/references/helper-steps/useQueryGraphStep",
depth: 4
depth: 5
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart.",
link: "/references/medusa-workflows/steps/releaseLockStep",
depth: 6
}
]
}}
@@ -768,15 +782,22 @@ You already have all the necessary steps within the workflow, so you create it r
Create the file `src/workflows/custom-add-to-cart.ts` with the following content:
export const customAddToCartWorkflowHighlights = [
["17", "useQueryGraphStep", "Get the cart's region."],
["27", "getCustomPriceWorkflow", "Get the custom price for the product variant."],
["47", "addToCartWorkflow", "Add the product variant to the cart with the custom price."],
["55", "useQueryGraphStep", "Get the updated cart data."]
["22", "useQueryGraphStep", "Get the cart's region."],
["32", "getCustomPriceWorkflow", "Get the custom price for the product variant."],
["40", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
["58", "addToCartWorkflow", "Add the product variant to the cart with the custom price."],
["66", "useQueryGraphStep", "Get the updated cart data."],
["74", "releaseLockStep", "Release the lock on the cart."]
]
```ts title="src/workflows/custom-add-to-cart.ts" highlights={customAddToCartWorkflowHighlights}
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import {
addToCartWorkflow,
acquireLockStep,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { getCustomPriceWorkflow } from "./get-custom-price"
type CustomAddToCartWorkflowInput = {
@@ -808,6 +829,12 @@ export const customAddToCartWorkflow = createWorkflow(
metadata: input.item.metadata,
},
})
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const itemData = transform({
item: input.item,
@@ -837,6 +864,10 @@ export const customAddToCartWorkflow = createWorkflow(
},
}).config({ name: "refetch-cart" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
cart: updatedCart[0],
})
@@ -850,10 +881,12 @@ In the workflow, you:
1. Retrieve the cart's region using the `useQueryGraphStep`.
2. Calculate the custom price using the `getCustomPriceWorkflow` that you created earlier.
3. Prepare the data of the item to add to the cart.
3. Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
4. Prepare the data of the item to add to the cart.
- You use the `transform` function because direct data manipulation isn't allowed in workflows. Refer to the [Data Manipulation](!docs!/learn/fundamentals/workflows/variable-manipulation) guide to learn more.
4. Add the item to the cart using the `addToCartWorkflow`.
5. Refetch the updated cart using the `useQueryGraphStep` to return the cart data in the workflow's response.
5. Add the item to the cart using the `addToCartWorkflow`.
6. Refetch the updated cart using the `useQueryGraphStep` to return the cart data in the workflow's response.
7. Release the lock on the cart using the `releaseLockStep`.
### b. Create the Add-to-Cart API Route

View File

@@ -732,6 +732,9 @@ export const createSubscriptionWorkflowHighlights = [
import {
createWorkflow,
WorkflowResponse,
useQueryGraphStep,
acquireLockStep,
releaseLockStep,
} from "@medusajs/framework/workflows-sdk"
import {
createRemoteLinkStep,
@@ -754,6 +757,11 @@ type WorkflowInput = {
const createSubscriptionWorkflow = createWorkflow(
"create-subscription",
(input: WorkflowInput) => {
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const { id } = completeCartWorkflow.runAsStep({
input: {
id: input.cart_id,
@@ -815,14 +823,34 @@ const createSubscriptionWorkflow = createWorkflow(
},
})
const { subscription, linkDefs } = createSubscriptionStep({
cart_id: input.cart_id,
order_id: orders[0].id,
customer_id: orders[0].customer_id!,
subscription_data: input.subscription_data,
const { data: existingLinks } = useQueryGraphStep({
entity: subscriptionOrderLink.entryPoint,
fields: ["subscription.id"],
filters: { order_id: orders[0].id },
}).config({ name: "retrieve-existing-links" })
const subscription = when(
"create-subscription-condition",
{ existingLinks },
(data) => data.existingLinks.length === 0
)
.then(() => {
const { subscription, linkDefs } = createSubscriptionStep({
cart_id: input.cart_id,
order_id: orders[0].id,
customer_id: orders[0].customer_id!,
subscription_data: input.subscription_data,
})
createRemoteLinkStep(linkDefs)
return subscription
})
createRemoteLinkStep(linkDefs)
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
subscription: subscription,
@@ -836,10 +864,14 @@ export default createSubscriptionWorkflow
This workflow accepts the carts ID, along with the subscription details. It executes the following steps:
1. `completeCartWorkflow` from `@medusajs/medusa/core-flows` that completes a cart and creates an order.
2. `useQueryGraphStep` from `@medusajs/medusa/core-flows` to retrieve the order's details. [Query](!docs!/learn/fundamentals/module-links/query) is a tool that allows you to retrieve data across modules.
3. `createSubscriptionStep`, which is the step you created previously.
4. `createRemoteLinkStep` from `@medusajs/medusa/core-flows`, which accepts links to create. These links are in the `linkDefs` array returned by the previous step.
1. [acquireLockStep](/references/medusa-workflows/steps/acquireLockStep) to acquire a lock on the cart to prevent race conditions.
2. [completeCartWorkflow](/references/medusa-workflows/completeCartWorkflow) that completes a cart and creates an order.
3. [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) to retrieve the order's details. [Query](!docs!/learn/fundamentals/module-links/query) is a tool that allows you to retrieve data across modules.
4. [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) again to check if a subscription already exists for the order. This is necessary to ensure idempotency in case the workflow is retried.
5. Use `when` to check if a subscription already exists for the order. If not, it executes the next steps:
1. `createSubscriptionStep`, which is the step you created previously.
2. `createRemoteLinkStep` which accepts links to create. These links are in the `linkDefs` array returned by the previous step.
6. [releaseLockStep](/references/medusa-workflows/steps/releaseLockStep) to release the lock on the cart.
The workflow returns the created subscription and order.
@@ -918,7 +950,7 @@ export const POST = async (
res.json({
type: "order",
...result,
order: result.order,
})
}
```
@@ -927,7 +959,7 @@ Since the file exports a `POST` function, you're exposing a `POST` API route at
In the route handler function, you retrieve the cart to access it's `metadata` property. If the subscription details aren't stored there, you throw an error.
Then, you use the `createSubscriptionWorkflow` you created to create the order, and return the created order and subscription in the response.
Then, you use the `createSubscriptionWorkflow` you created to create the order, and return the created order in the response.
In the next step, you'll customize the Next.js Starter Storefront, allowing you to test out the subscription feature.
@@ -965,6 +997,7 @@ In this step, you'll customize the checkout flow in the [Next.js Starter storefr
1. Add a subscription step to the checkout flow.
2. Pass the additional data that Stripe requires to later capture the payment when the subscription renews, as explained in the [Payment Flow Overview](#intermission-payment-flow-overview).
3. Change the complete cart action to use the custom API route you created in the previous step.
### Add Subscription Step
@@ -1199,6 +1232,30 @@ If you're integrating with a custom payment provider, you can instead pass the r
The payment method can now be used later to capture the payment when the subscription renews.
### Change Complete Cart Action
Finally, you need to change the complete cart action to use the custom API route you created in the previous step.
In `src/lib/data/cart.ts`, find the `placeOrder` function and change the `await sdk.store.cart.complete` call to the following:
```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="orange"
const cartRes = await sdk.client.fetch<{
type: "cart"
cart: HttpTypes.StoreCart
} | {
type: "order"
order: HttpTypes.StoreOrder
}>(
`/store/carts/${id}/subscribe`,
{
method: "POST",
headers,
}
)
```
You change the request to send a `POST` request to the `/store/carts/[id]/subscribe` endpoint you created earlier. You can keep the rest of the `placeOrder` function as is.
### Test Cart Completion and Subscription Creation
To test out the cart completion flow:

View File

@@ -3236,7 +3236,7 @@ const handleStep2Submit = async () => {
return price
}).filter((price) => price.amount > 0), // Only include prices > 0
}))
})).filter((variant) => variant.seat_count > 0) // Only create variants for row types with seats
setIsLoading(true)
try {
@@ -3499,16 +3499,14 @@ You can edit the associated Medusa product to add images, descriptions, and othe
---
## Step 10: Validate Cart Before Checkout
## Step 10: Validate Ticket on Add to Cart
In this step, you'll add custom validation to core cart operations that ensures a seat isn't purchased more than once for the same date.
In this step, you'll add custom validation to the core add-to-cart operation that ensures a seat isn't purchased more than once for the same date.
Medusa implements cart operations in workflows. Specifically, you'll focus on the `addToCartWorkflow` and `completeCartWorkflow`. Medusa allows you to inject custom logic into workflows using [hooks](!docs!/learn/fundamentals/workflows/workflow-hooks).
Medusa implements cart operations in workflows. Specifically, you'll focus on the `addToCartWorkflow`. Medusa allows you to inject custom logic into workflows using [hooks](!docs!/learn/fundamentals/workflows/workflow-hooks).
A workflow hook is a point in a workflow where you can inject custom functionality as a step function.
#### Add to Cart Validation Hook
To consume the `validate` hook of the `addToCartWorkflow` that holds the add-to-cart logic, create the file `src/workflows/hooks/add-to-cart-validation.ts` with the following content:
```ts title="src/workflows/hooks/add-to-cart-validation.ts"
@@ -3606,91 +3604,7 @@ In the step function, you:
If the hook throws an error, the add-to-cart operation will be aborted and the error message will be returned to the client.
#### Complete Cart Validation Hook
Next, to consume the `validate` hook of the `completeCartWorkflow` that holds the checkout logic, create the file `src/workflows/hooks/complete-cart-validation.ts` with the following content:
```ts title="src/workflows/hooks/complete-cart-validation.ts"
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
import { MedusaError } from "@medusajs/framework/utils"
completeCartWorkflow.hooks.validate(
async ({ cart }, { container }) => {
const query = container.resolve("query")
const { data: items } = await query.graph({
entity: "line_item",
fields: ["id", "variant_id", "metadata", "quantity"],
filters: {
id: cart.items.map((item) => item.id).filter(Boolean) as string[],
},
})
// Get the product variant to check if it's a ticket product variant
const { data: productVariants } = await query.graph({
entity: "product_variant",
fields: ["id", "product_id", "ticket_product_variant.purchases.*"],
filters: {
id: items.map((item) => item.variant_id).filter(Boolean) as string[],
},
})
// Check for duplicate seats within the cart
const seatDateCombinations = new Set<string>()
for (const item of items) {
if (item.quantity !== 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"You can only purchase one ticket for a seat."
)
}
const productVariant = productVariants.find(
(variant) => variant.id === item.variant_id
)
if (!productVariant || !item.metadata?.seat_number) {continue}
if (!item.metadata?.show_date) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Show date is required for seat ${item.metadata?.seat_number} in product ${productVariant.product_id}`
)
}
// Create a unique key for seat and date combination
const seatDateKey = `${item.metadata?.seat_number}-${item.metadata?.show_date}`
// Check if this seat-date combination already exists in the cart
if (seatDateCombinations.has(seatDateKey)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Duplicate seat ${item.metadata?.seat_number} found for show date ${item.metadata?.show_date} in cart`
)
}
// Add to the set to track this combination
seatDateCombinations.add(seatDateKey)
// Check if seat has already been purchased
const existingPurchase = productVariant.ticket_product_variant?.purchases.find(
(purchase) => purchase?.seat_number === item.metadata?.seat_number
&& purchase?.show_date === item.metadata?.show_date
)
if (existingPurchase) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
)
}
}
}
)
```
Similar to the previous hook, you consume the `validate` hook of the `completeCartWorkflow` to validate that no seat is purchased more than once for the same date.
You can test out both hooks when you [customize the storefront](./storefront/page.mdx).
You can test out the hook when you [customize the storefront](./storefront/page.mdx).
---
@@ -3706,46 +3620,80 @@ The custom workflow that completes the cart has the following steps:
workflow={{
name: "completeCartWithTicketsWorkflow",
steps: [
{
type: "step",
name: "acquireLockStep",
description: "Acquire a lock on the cart to prevent concurrent modifications",
link: "/references/medusa-workflows/steps/acquireLockStep",
depth: 1,
},
{
type: "workflow",
name: "completeCartWorkflow",
description: "Complete the cart using Medusa's default completeCartWorkflow",
depth: 1,
depth: 2,
link: "/references/medusa-workflows/completeCartWorkflow"
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the cart details",
depth: 2,
depth: 3,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "createTicketPurchasesStep",
description: "Create ticket purchases for each ticket product variant in the cart",
depth: 3,
name: "useQueryGraphStep",
description: "Retrieve existing ticket purchases to ensure idempotency",
depth: 4,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "createRemoteLinkStep",
description: "Create links between the order and ticket purchases",
depth: 4,
link: "/references/helper-steps/createRemoteLinkStep"
type: "when",
condition: "existingLinks.length === 0",
steps: [
{
type: "step",
name: "validateTicketOrderStep",
description: "Validate that the ticket order can be processed",
depth: 1,
},
{
type: "step",
name: "createTicketPurchasesStep",
description: "Create ticket purchases for each ticket product variant in the cart",
depth: 2,
},
{
type: "step",
name: "createRemoteLinkStep",
description: "Create links between the order and ticket purchases",
depth: 3,
link: "/references/helper-steps/createRemoteLinkStep"
},
],
depth: 5
},
{
type: "step",
name: "useQueryGraphStep",
description: "Retrieve the order details",
depth: 5,
depth: 6,
link: "/references/helper-steps/useQueryGraphStep"
},
{
type: "step",
name: "releaseLockStep",
description: "Release the lock on the cart",
depth: 7,
link: "/references/medusa-workflows/steps/releaseLockStep"
}
]
}}
hideLegend
/>
You only need to implement the `createTicketPurchasesStep` step, as the other steps and workflows are provided by Medusa.
You only need to implement the `createTicketPurchasesStep` and `validateTicketOrderStep` steps, as the other steps and workflows are provided by Medusa.
#### createTicketPurchasesStep
@@ -3840,18 +3788,135 @@ In the step function, you prepare the ticket purchases to be created, create the
In the compensation function, you delete the created ticket purchases if an error occurs in the workflow.
#### validateTicketOrderStep
The `validateTicketOrderStep` validates that the tickets can be purchased based on their availability.
To create the step, create the file `src/workflows/steps/validate-ticket-order.ts` with the following content:
```ts title="src/workflows/steps/validate-ticket-order.ts"
import { MedusaError } from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
export type ValidateTicketOrderStepInput = {
items: {
id: string
variant_id: string
metadata: Record<string, unknown>
quantity: number
variant?: {
id: string
product_id: string
ticket_product_variant?: {
purchases?: {
seat_number: string
show_date: Date
}[]
}
}
}[]
order_id: string
}
export const validateTicketOrderStep = createStep(
"validate-ticket-order",
async ({ items, order_id }: ValidateTicketOrderStepInput, { container }) => {
// Check for duplicate seats within the cart
const seatDateCombinations = new Set<string>()
for (const item of items) {
if (item.quantity !== 1) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"You can only purchase one ticket for a seat."
)
}
if (!item.variant || !item.metadata?.seat_number) {continue}
if (!item.metadata?.show_date) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Show date is required for seat ${item.metadata?.seat_number} in product ${item.variant.product_id}`
)
}
// Create a unique key for seat and date combination
const seatDateKey = `${item.metadata?.seat_number}-${item.metadata?.show_date}`
// Check if this seat-date combination already exists in the cart
if (seatDateCombinations.has(seatDateKey)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Duplicate seat ${item.metadata?.seat_number} found for show date ${item.metadata?.show_date} in cart`
)
}
// Add to the set to track this combination
seatDateCombinations.add(seatDateKey)
// Check if seat has already been purchased
const existingPurchase = item.variant.ticket_product_variant?.purchases?.find(
(purchase) => purchase?.seat_number === item.metadata?.seat_number
&& purchase?.show_date === item.metadata?.show_date
)
if (existingPurchase) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
)
}
}
return new StepResponse({ validated: true }, order_id)
},
async (order_id, { container, context }) => {
if (!order_id) {return}
cancelOrderWorkflow(container).run({
input: {
order_id,
},
context,
container,
})
}
)
```
The `validateTicketOrderStep` accepts the cart items and order ID as input.
In the step function, you validate that:
1. No seat is purchased more than once for the same date within the cart.
2. No seat has already been purchased for the same date.
If any validation fails, you throw an error to abort the workflow.
The step also has a compensation function that cancels the order if an error occurs later in the workflow. This is important to ensure that if ticket purchase creation fails, the order is not left in a completed state.
#### Custom Complete Cart Workflow
You can now create the custom workflow that completes the cart and creates ticket purchases.
Create the file `src/workflows/complete-cart-with-tickets.ts` with the following content:
```ts title="src/workflows/complete-cart-with-tickets.ts"
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { completeCartWorkflow, createRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
```ts title="src/workflows/complete-cart-with-tickets.ts" collapsibleLines="1-14" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import {
completeCartWorkflow,
createRemoteLinkStep,
acquireLockStep,
releaseLockStep,
useQueryGraphStep,
} from "@medusajs/medusa/core-flows"
import { createTicketPurchasesStep, CreateTicketPurchasesStepInput } from "./steps/create-ticket-purchases"
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"
import { Modules } from "@medusajs/framework/utils"
import ticketPurchaseOrderLink from "../links/ticket-purchase-order"
import { validateTicketOrderStep, ValidateTicketOrderStepInput } from "./steps/validate-ticket-order"
export type CompleteCartWithTicketsWorkflowInput = {
cart_id: string
@@ -3860,7 +3925,11 @@ export type CompleteCartWithTicketsWorkflowInput = {
export const completeCartWithTicketsWorkflow = createWorkflow(
"complete-cart-with-tickets",
(input: CompleteCartWithTicketsWorkflowInput) => {
// Step 1: Complete the cart using Medusa's workflow
acquireLockStep({
key: input.cart_id,
timeout: 2,
ttl: 10,
})
const order = completeCartWorkflow.runAsStep({
input: {
id: input.cart_id,
@@ -3876,7 +3945,9 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
"items.variant.options.option.*",
"items.variant.ticket_product_variant.*",
"items.variant.ticket_product_variant.ticket_product.*",
"items.variant.ticket_product_variant.purchases.*",
"items.metadata",
"items.quantity",
],
filters: {
id: input.cart_id,
@@ -3886,31 +3957,40 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
},
})
// Step 2: Create ticket purchases for ticket products
const ticketPurchases = createTicketPurchasesStep({
order_id: order.id,
cart: carts[0],
} as unknown as CreateTicketPurchasesStepInput)
const { data: existingLinks } = useQueryGraphStep({
entity: ticketPurchaseOrderLink.entryPoint,
fields: ["ticket_purchase.id"],
filters: { order_id: order.id },
}).config({ name: "retrieve-existing-links" })
// Step 3: Link ticket purchases to the order
const linkData = transform({
order,
ticketPurchases,
}, (data) => {
return data.ticketPurchases.map((purchase) => ({
[TICKET_BOOKING_MODULE]: {
ticket_purchase_id: purchase.id,
},
[Modules.ORDER]: {
order_id: data.order.id,
},
}))
when({ existingLinks }, (data) => data.existingLinks.length === 0)
.then(() => {
validateTicketOrderStep({
items: carts[0].items,
order_id: order.id,
} as unknown as ValidateTicketOrderStepInput)
const ticketPurchases = createTicketPurchasesStep({
order_id: order.id,
cart: carts[0],
} as unknown as CreateTicketPurchasesStepInput)
const linkData = transform({
order,
ticketPurchases,
}, (data) => {
return data.ticketPurchases.map((purchase) => ({
[TICKET_BOOKING_MODULE]: {
ticket_purchase_id: purchase.id,
},
[Modules.ORDER]: {
order_id: data.order.id,
},
}))
})
createRemoteLinkStep(linkData)
})
// Step 4: Create remote links
createRemoteLinkStep(linkData)
// Step 5: Fetch order details
const { data: refetchedOrder } = useQueryGraphStep({
entity: "order",
fields: [
@@ -3934,6 +4014,10 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
},
}).config({ name: "refetch-order" })
releaseLockStep({
key: input.cart_id,
})
return new WorkflowResponse({
order: refetchedOrder[0],
})
@@ -3945,11 +4029,17 @@ The `completeCartWithTicketsWorkflow` accepts the cart ID as input.
In the workflow function, you:
1. Complete the cart using Medusa's `completeCartWorkflow`.
2. Retrieve the cart details using the `useQueryGraphStep`.
3. Create ticket purchases for each ticket product variant in the cart using the `createTicketPurchasesStep`.
4. Create links between the order and the created ticket purchases using the `createRemoteLinkStep`.
5. Retrieve the order details using the `useQueryGraphStep`.
1. Acquire a lock on the cart to prevent concurrent modifications using the `acquireLockStep`.
2. Complete the cart using Medusa's `completeCartWorkflow`.
3. Retrieve the cart details using the `useQueryGraphStep`.
4. Retrieve existing ticket purchases linked to the order to ensure idempotency using the `useQueryGraphStep`.
- This is important because if the workflow is retried, you don't want to create duplicate ticket purchases.
5. Use `when` to check that there are no existing links between the order and ticket purchases. If so, you:
1. Validate that the ticket order can be processed using the `validateTicketOrderStep`.
2. Create ticket purchases for each ticket product variant in the cart using the `createTicketPurchasesStep`.
3. Create links between the order and the created ticket purchases using the `createRemoteLinkStep`.
6. Retrieve the order details using the `useQueryGraphStep`.
7. Release the lock on the cart using the `releaseLockStep`.
Finally, you return the order details.

View File

@@ -110,17 +110,17 @@ export const generatedEditDates = {
"app/nextjs-starter/page.mdx": "2025-02-26T11:37:47.137Z",
"app/recipes/b2b/page.mdx": "2025-05-20T07:51:40.718Z",
"app/recipes/commerce-automation/page.mdx": "2025-05-20T07:51:40.719Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-10-21T10:31:35.296Z",
"app/recipes/digital-products/examples/standard/page.mdx": "2025-11-28T09:43:09.959Z",
"app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z",
"app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-10-21T10:47:23.415Z",
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-11-28T10:09:41.993Z",
"app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z",
"app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z",
"app/recipes/oms/page.mdx": "2025-05-20T07:51:40.722Z",
"app/recipes/personalized-products/page.mdx": "2025-07-22T06:52:29.419Z",
"app/recipes/pos/page.mdx": "2025-05-20T07:51:40.723Z",
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-10-21T12:36:28.745Z",
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-11-28T12:02:36.170Z",
"app/recipes/subscriptions/page.mdx": "2025-05-20T07:51:40.723Z",
"app/recipes/page.mdx": "2025-05-20T07:51:40.722Z",
"app/service-factory-reference/methods/create/page.mdx": "2025-09-01T15:54:53.385Z",
@@ -5864,7 +5864,7 @@ export const generatedEditDates = {
"app/commerce-modules/payment/account-holder/page.mdx": "2025-04-07T07:31:20.235Z",
"app/troubleshooting/test-errors/page.mdx": "2025-10-28T16:02:39.347Z",
"app/commerce-modules/product/variant-inventory/page.mdx": "2025-04-25T13:25:02.408Z",
"app/examples/guides/custom-item-price/page.mdx": "2025-06-26T11:53:06.748Z",
"app/examples/guides/custom-item-price/page.mdx": "2025-11-28T08:30:59.896Z",
"references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateShippingStep/page.mdx": "2025-04-11T09:04:35.729Z",
"references/core_flows/Cart/Steps_Cart/variables/core_flows.Cart.Steps_Cart.validateShippingStepId/page.mdx": "2025-02-11T11:36:39.228Z",
"references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createPaymentAccountHolderStep/page.mdx": "2025-02-24T10:48:31.714Z",
@@ -6042,7 +6042,7 @@ export const generatedEditDates = {
"app/integrations/guides/algolia/page.mdx": "2025-11-27T08:21:37.971Z",
"app/integrations/guides/magento/page.mdx": "2025-10-09T11:30:09.533Z",
"app/js-sdk/auth/overview/page.mdx": "2025-03-28T08:05:32.622Z",
"app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-10-09T11:27:14.961Z",
"app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-11-28T08:48:32.647Z",
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.plugin/page.mdx": "2025-04-11T09:04:55.084Z",
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/page.mdx": "2025-05-20T07:51:40.936Z",
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/page.mdx": "2025-04-11T09:04:54.015Z",
@@ -6473,7 +6473,7 @@ export const generatedEditDates = {
"references/types/interfaces/types.WebhookActionResult/page.mdx": "2025-05-20T07:51:41.086Z",
"references/types/interfaces/types.WebhookActionData/page.mdx": "2025-05-20T07:51:41.086Z",
"app/commerce-modules/tax/tax-provider/page.mdx": "2025-10-22T07:14:23.461Z",
"app/recipes/bundled-products/examples/standard/page.mdx": "2025-06-26T11:52:18.819Z",
"app/recipes/bundled-products/examples/standard/page.mdx": "2025-11-28T08:10:29.485Z",
"app/recipes/bundled-products/page.mdx": "2025-05-20T07:51:40.718Z",
"app/infrastructure-modules/analytics/local/page.mdx": "2025-08-21T05:30:26.867Z",
"app/infrastructure-modules/analytics/page.mdx": "2025-08-21T05:31:17.953Z",
@@ -6547,15 +6547,15 @@ export const generatedEditDates = {
"app/how-to-tutorials/tutorials/re-order/page.mdx": "2025-06-26T12:38:24.308Z",
"app/commerce-modules/promotion/promotion-taxes/page.mdx": "2025-06-27T15:44:46.638Z",
"app/troubleshooting/payment/page.mdx": "2025-07-16T10:20:24.799Z",
"app/recipes/personalized-products/example/page.mdx": "2025-10-31T16:39:17.453Z",
"app/how-to-tutorials/tutorials/preorder/page.mdx": "2025-10-09T11:33:06.479Z",
"app/recipes/personalized-products/example/page.mdx": "2025-11-28T10:13:26.457Z",
"app/how-to-tutorials/tutorials/preorder/page.mdx": "2025-11-28T10:55:29.378Z",
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.archive/page.mdx": "2025-09-12T14:10:49.018Z",
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.complete/page.mdx": "2025-09-12T14:10:49.024Z",
"app/commerce-modules/cart/cart-totals/page.mdx": "2025-07-31T15:18:13.978Z",
"app/commerce-modules/order/order-totals/page.mdx": "2025-07-31T15:12:10.633Z",
"app/commerce-modules/user/invite-user-subscriber/page.mdx": "2025-08-01T12:01:54.551Z",
"app/how-to-tutorials/tutorials/invoice-generator/page.mdx": "2025-10-09T11:26:43.515Z",
"app/how-to-tutorials/tutorials/product-builder/page.mdx": "2025-10-09T11:28:24.756Z",
"app/how-to-tutorials/tutorials/product-builder/page.mdx": "2025-11-28T11:00:31.676Z",
"app/integrations/guides/payload/page.mdx": "2025-10-22T15:05:39.648Z",
"references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken/page.mdx": "2025-08-14T12:59:55.678Z",
"app/commerce-modules/order/draft-orders/page.mdx": "2025-08-26T09:21:49.780Z",
@@ -6565,7 +6565,7 @@ export const generatedEditDates = {
"app/storefront-development/cart/manage-promotions/page.mdx": "2025-09-11T14:11:40.904Z",
"app/recipes/ticket-booking/examples/page.mdx": "2025-09-10T14:11:55.063Z",
"app/recipes/ticket-booking/examples/storefront/page.mdx": "2025-09-10T14:14:44.005Z",
"app/recipes/ticket-booking/example/page.mdx": "2025-09-10T15:13:15.604Z",
"app/recipes/ticket-booking/example/page.mdx": "2025-11-28T13:02:15.777Z",
"app/recipes/ticket-booking/example/storefront/page.mdx": "2025-09-10T15:26:04.084Z",
"app/recipes/ticket-booking/page.mdx": "2025-09-11T06:53:21.071Z",
"references/js_sdk/admin/Views/methods/js_sdk.admin.Views.columns/page.mdx": "2025-10-21T08:10:57.659Z",
@@ -6612,7 +6612,7 @@ export const generatedEditDates = {
"app/data-model-repository-reference/methods/upsert/page.mdx": "2025-10-28T16:02:28.730Z",
"app/data-model-repository-reference/methods/update/page.mdx": "2025-10-28T16:02:27.582Z",
"app/data-model-repository-reference/methods/upsertWithReplace/page.mdx": "2025-10-28T16:02:30.479Z",
"app/how-to-tutorials/tutorials/agentic-commerce/page.mdx": "2025-10-09T11:25:48.831Z",
"app/how-to-tutorials/tutorials/agentic-commerce/page.mdx": "2025-11-28T07:37:38.151Z",
"app/storefront-development/production-optimizations/page.mdx": "2025-10-03T13:28:37.909Z",
"app/how-to-tutorials/tutorials/category-images/page.mdx": "2025-11-07T08:55:59.228Z",
"app/infrastructure-modules/caching/page.mdx": "2025-11-13T14:18:03.173Z",
@@ -6682,7 +6682,7 @@ export const generatedEditDates = {
"references/utils/PromotionUtils/enums/utils.PromotionUtils.ApplicationMethodAllocation/page.mdx": "2025-10-21T08:10:52.665Z",
"references/utils/PromotionUtils/enums/utils.PromotionUtils.CampaignBudgetType/page.mdx": "2025-10-21T08:10:52.672Z",
"app/integrations/guides/avalara/page.mdx": "2025-10-22T09:56:11.929Z",
"app/how-to-tutorials/tutorials/product-rentals/page.mdx": "2025-10-28T16:09:26.244Z",
"app/how-to-tutorials/tutorials/product-rentals/page.mdx": "2025-11-28T11:39:49.672Z",
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchImageVariants/page.mdx": "2025-10-31T09:41:42.515Z",
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantImages/page.mdx": "2025-10-31T09:41:42.517Z",
"references/product/IProductModuleService/methods/product.IProductModuleService.addImageToVariant/page.mdx": "2025-10-31T09:41:35.918Z",
@@ -6718,6 +6718,6 @@ export const generatedEditDates = {
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchImageVariantsWorkflow/page.mdx": "2025-11-05T12:22:20.639Z",
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchVariantImagesWorkflow/page.mdx": "2025-11-05T12:22:20.671Z",
"app/storefront-development/guides/react-native-expo/page.mdx": "2025-11-06T07:18:45.347Z",
"app/how-to-tutorials/tutorials/customer-tiers/page.mdx": "2025-11-25T08:24:24.566Z",
"app/how-to-tutorials/tutorials/customer-tiers/page.mdx": "2025-11-28T08:34:06.912Z",
"app/infrastructure-modules/caching/guides/clear-cache/page.mdx": "2025-11-26T13:19:26.629Z"
}