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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
@@ -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.