docs: update recipes and tutorials to support locks and idempotency (#14151)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user