docs: update recipes and tutorials to support locks and idempotency (#14151)
This commit is contained in:
@@ -375,36 +375,50 @@ The workflow you'll implement in this section has the following steps:
|
|||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep (Retrieve Variant)",
|
name: "useQueryGraphStep (Retrieve Variant)",
|
||||||
description: "Retrieve the variant's details using Query",
|
description: "Retrieve the variant's details using Query",
|
||||||
depth: 1,
|
depth: 2,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "getVariantMetalPricesStep",
|
name: "getVariantMetalPricesStep",
|
||||||
description: "Retrieve the variant's price using the third-party service.",
|
description: "Retrieve the variant's price using the third-party service.",
|
||||||
depth: 1,
|
depth: 3,
|
||||||
link: "#getvariantmetalpricesstep"
|
link: "#getvariantmetalpricesstep"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "acquireLockStep",
|
||||||
|
description: "Acquire a lock on the cart to prevent concurrent modifications.",
|
||||||
|
depth: 4,
|
||||||
|
link: "/references/medusa-workflows/steps/acquireLockStep",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "addToCartWorkflow",
|
name: "addToCartWorkflow",
|
||||||
description: "Add the item with the custom price to the cart.",
|
description: "Add the item with the custom price to the cart.",
|
||||||
depth: 1,
|
depth: 5,
|
||||||
link: "/references/medusa-workflows/addToCartWorkflow"
|
link: "/references/medusa-workflows/addToCartWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep (Retrieve Cart)",
|
name: "useQueryGraphStep (Retrieve Cart)",
|
||||||
description: "Retrieve the updated cart's details using Query.",
|
description: "Retrieve the updated cart's details using Query.",
|
||||||
depth: 1,
|
depth: 6,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "releaseLockStep",
|
||||||
|
description: "Release the lock on the cart.",
|
||||||
|
depth: 7,
|
||||||
|
link: "/references/medusa-workflows/steps/releaseLockStep",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
hideLegend
|
hideLegend
|
||||||
/>
|
/>
|
||||||
|
|
||||||
`useQueryGraphStep` and `addToCartWorkflow` are available through Medusa's core workflows package. You'll only implement the `getVariantMetalPricesStep`.
|
You'll only implement the `getVariantMetalPricesStep`. Medusa provides the other steps out-of-the-box.
|
||||||
|
|
||||||
### getVariantMetalPricesStep
|
### getVariantMetalPricesStep
|
||||||
|
|
||||||
@@ -519,7 +533,7 @@ Create the file `src/workflows/add-custom-to-cart.ts` with the following content
|
|||||||
|
|
||||||
export const workflowHighlights = [
|
export const workflowHighlights = [
|
||||||
["17", "useQueryGraphStep", "Retrieve the cart's details."],
|
["17", "useQueryGraphStep", "Retrieve the cart's details."],
|
||||||
["23", "useQueryGraphStep", "Retrieve the variant's details."],
|
["26", "useQueryGraphStep", "Retrieve the variant's details."],
|
||||||
]
|
]
|
||||||
|
|
||||||
```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights}
|
```ts title="src/workflows/add-custom-to-cart.ts" highlights={workflowHighlights}
|
||||||
@@ -543,6 +557,9 @@ export const addCustomToCartWorkflow = createWorkflow(
|
|||||||
entity: "cart",
|
entity: "cart",
|
||||||
filters: { id: cart_id },
|
filters: { id: cart_id },
|
||||||
fields: ["id", "currency_code"],
|
fields: ["id", "currency_code"],
|
||||||
|
options: {
|
||||||
|
throwIfKeyNotFound: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: variants } = useQueryGraphStep({
|
const { data: variants } = useQueryGraphStep({
|
||||||
@@ -607,7 +624,10 @@ Next, you'll add the item with the custom price to the cart. First, add the foll
|
|||||||
|
|
||||||
```ts title="src/workflows/add-custom-to-cart.ts"
|
```ts title="src/workflows/add-custom-to-cart.ts"
|
||||||
import { transform } from "@medusajs/framework/workflows-sdk"
|
import { transform } from "@medusajs/framework/workflows-sdk"
|
||||||
import { addToCartWorkflow } from "@medusajs/medusa/core-flows"
|
import {
|
||||||
|
acquireLockStep,
|
||||||
|
addToCartWorkflow,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, replace the `TODO` in the workflow with the following:
|
Then, replace the `TODO` in the workflow with the following:
|
||||||
@@ -623,6 +643,12 @@ const itemToAdd = transform({
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
addToCartWorkflow.runAsStep({
|
addToCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
items: itemToAdd,
|
items: itemToAdd,
|
||||||
@@ -633,7 +659,9 @@ addToCartWorkflow.runAsStep({
|
|||||||
// TODO retrieve and return cart
|
// TODO retrieve and return cart
|
||||||
```
|
```
|
||||||
|
|
||||||
You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow. After that, you use Medusa's `addToCartWorkflow` to add the item with the custom price to the cart.
|
You prepare the item to add to the cart using `transform` from the Workflows SDK. It allows you to manipulate and create variables in a workflow.
|
||||||
|
|
||||||
|
After that, you use Medusa's `acquireLockStep` to acquire a lock on the cart, and `addToCartWorkflow` to add the item with the custom price to the cart.
|
||||||
|
|
||||||
<Note title="Tip">
|
<Note title="Tip">
|
||||||
|
|
||||||
@@ -645,6 +673,9 @@ Lastly, you'll retrieve the cart's details again and return them. Add the follow
|
|||||||
|
|
||||||
```ts title="src/workflows/add-custom-to-cart.ts"
|
```ts title="src/workflows/add-custom-to-cart.ts"
|
||||||
import { WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
import { WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
import {
|
||||||
|
releaseLockStep,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
```
|
```
|
||||||
|
|
||||||
And replace the last `TODO` in the workflow with the following:
|
And replace the last `TODO` in the workflow with the following:
|
||||||
@@ -656,12 +687,18 @@ const { data: updatedCarts } = useQueryGraphStep({
|
|||||||
fields: ["id", "items.*"],
|
fields: ["id", "items.*"],
|
||||||
}).config({ name: "refetch-cart" })
|
}).config({ name: "refetch-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
cart: updatedCarts[0],
|
cart: updatedCarts[0],
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.
|
In the code above, you retrieve the updated cart's details using the `useQueryGraphStep` helper step. Then, you release the lock on the cart with the `releaseLockStep`.
|
||||||
|
|
||||||
|
To return data from the workflow, you create and return a `WorkflowResponse` instance. It accepts as a parameter the data to return, which is the updated cart.
|
||||||
|
|
||||||
In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart.
|
In the next step, you'll use the workflow in a custom route to add an item with a custom price to the cart.
|
||||||
|
|
||||||
|
|||||||
@@ -1186,12 +1186,26 @@ The workflow has the following steps:
|
|||||||
link: "/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow",
|
link: "/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow",
|
||||||
depth: 1
|
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",
|
type: "workflow",
|
||||||
name: "addShippingMethodToCartWorkflow",
|
name: "addShippingMethodToCartWorkflow",
|
||||||
description: "Add shipping method to the cart",
|
description: "Add shipping method to the cart",
|
||||||
link: "/references/medusa-workflows/addShippingMethodToCartWorkflow",
|
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
|
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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
@@ -1218,11 +1232,13 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
addShippingMethodToCartWorkflow,
|
addShippingMethodToCartWorkflow,
|
||||||
createCartWorkflow,
|
createCartWorkflow,
|
||||||
CreateCartWorkflowInput,
|
CreateCartWorkflowInput,
|
||||||
createCustomersWorkflow,
|
createCustomersWorkflow,
|
||||||
listShippingOptionsForCartWithPricingWorkflow,
|
listShippingOptionsForCartWithPricingWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
import {
|
import {
|
||||||
@@ -1463,9 +1479,17 @@ when(input, (input) => !!input.fulfillment_address)
|
|||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
acquireLockStep({
|
||||||
|
key: createdCart.id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
addShippingMethodToCartWorkflow.runAsStep({
|
addShippingMethodToCartWorkflow.runAsStep({
|
||||||
input: shippingMethodData,
|
input: shippingMethodData,
|
||||||
})
|
})
|
||||||
|
releaseLockStep({
|
||||||
|
key: createdCart.id,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO prepare checkout session response
|
// 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).
|
- Retrieve the shipping options using the [listShippingOptionsForCartWithPricingWorkflow](/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow).
|
||||||
- Create a variable with the cheapest shipping option using the `transform` function.
|
- 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).
|
- 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
|
#### Prepare Checkout Session Response
|
||||||
|
|
||||||
@@ -1895,12 +1921,19 @@ The workflow has the following steps:
|
|||||||
],
|
],
|
||||||
depth: 4
|
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",
|
type: "workflow",
|
||||||
name: "updateCartWorkflow",
|
name: "updateCartWorkflow",
|
||||||
description: "Update the cart with the new data",
|
description: "Update the cart with the new data",
|
||||||
link: "/references/medusa-workflows/updateCartWorkflow",
|
link: "/references/medusa-workflows/updateCartWorkflow",
|
||||||
depth: 5
|
depth: 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
type: "when",
|
||||||
@@ -1914,13 +1947,20 @@ The workflow has the following steps:
|
|||||||
depth: 1
|
depth: 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 6
|
depth: 7
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
name: "prepareCheckoutSessionDataWorkflow",
|
name: "prepareCheckoutSessionDataWorkflow",
|
||||||
description: "Prepare the checkout session response",
|
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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
@@ -1938,8 +1978,10 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
addShippingMethodToCartWorkflow,
|
addShippingMethodToCartWorkflow,
|
||||||
createCustomersWorkflow,
|
createCustomersWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
updateCartWorkflow,
|
updateCartWorkflow,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} 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:
|
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"
|
```ts title="src/workflows/update-checkout-session.ts"
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
// Prepare update data
|
// Prepare update data
|
||||||
const updateData = transform({
|
const updateData = transform({
|
||||||
input,
|
input,
|
||||||
@@ -2106,9 +2153,11 @@ updateCartWorkflow.runAsStep({
|
|||||||
// TODO add shipping method if fulfillment option ID is provided
|
// 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
|
#### 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)
|
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.
|
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
|
### 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",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 1,
|
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",
|
type: "when",
|
||||||
condition: "!!input.payment_data.billing_address",
|
condition: "!!input.payment_data.billing_address",
|
||||||
@@ -2437,7 +2499,7 @@ The workflow that completes a checkout session has the following steps:
|
|||||||
depth: 1
|
depth: 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 2
|
depth: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
type: "when",
|
||||||
@@ -2471,7 +2533,7 @@ The workflow that completes a checkout session has the following steps:
|
|||||||
depth: 4
|
depth: 4
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 3
|
depth: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
type: "when",
|
||||||
@@ -2484,7 +2546,14 @@ The workflow that completes a checkout session has the following steps:
|
|||||||
depth: 1
|
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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
@@ -2502,10 +2571,12 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
completeCartWorkflow,
|
completeCartWorkflow,
|
||||||
createPaymentCollectionForCartWorkflow,
|
createPaymentCollectionForCartWorkflow,
|
||||||
createPaymentSessionsWorkflow,
|
createPaymentSessionsWorkflow,
|
||||||
refreshPaymentCollectionForCartWorkflow,
|
refreshPaymentCollectionForCartWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
updateCartWorkflow,
|
updateCartWorkflow,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
@@ -2556,6 +2627,11 @@ export const completeCheckoutSessionWorkflow = createWorkflow(
|
|||||||
throwIfKeyNotFound: true,
|
throwIfKeyNotFound: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
// TODO update cart with billing address if provided
|
// 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.
|
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
|
#### Update Cart with Billing Address
|
||||||
|
|
||||||
@@ -2744,10 +2820,16 @@ const responseData = transform({
|
|||||||
return data.completeCartResponse || data.invalidPaymentResponse
|
return data.completeCartResponse || data.invalidPaymentResponse
|
||||||
})
|
})
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(responseData)
|
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
|
### 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",
|
description: "Validate if the cart can be canceled",
|
||||||
depth: 2
|
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",
|
type: "when",
|
||||||
condition: "!!data.carts[0].payment_collection?.payment_sessions?.length",
|
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: 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 3
|
depth: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
name: "updateCartWorkflow",
|
name: "updateCartWorkflow",
|
||||||
description: "Update the cart status to canceled",
|
description: "Update the cart status to canceled",
|
||||||
link: "/references/medusa-workflows/updateCartWorkflow",
|
link: "/references/medusa-workflows/updateCartWorkflow",
|
||||||
depth: 4
|
depth: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
name: "prepareCheckoutSessionDataWorkflow",
|
name: "prepareCheckoutSessionDataWorkflow",
|
||||||
description: "Prepare the checkout session response",
|
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"
|
```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 { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
import { validateCartCancelationStep, ValidateCartCancelationStepInput } from "./steps/validate-cart-cancelation"
|
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 { cancelPaymentSessionsStep } from "./steps/cancel-payment-sessions"
|
||||||
import { prepareCheckoutSessionDataWorkflow } from "./prepare-checkout-session-data"
|
import { prepareCheckoutSessionDataWorkflow } from "./prepare-checkout-session-data"
|
||||||
|
|
||||||
@@ -3204,6 +3300,12 @@ export const cancelCheckoutSessionWorkflow = createWorkflow(
|
|||||||
cart: carts[0],
|
cart: carts[0],
|
||||||
} as unknown as ValidateCartCancelationStepInput)
|
} as unknown as ValidateCartCancelationStepInput)
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
// TODO cancel payment sessions if any
|
// 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.
|
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:
|
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.
|
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"
|
```ts title="src/workflows/cancel-checkout-session.ts"
|
||||||
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
|
const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
|
||||||
@@ -3255,10 +3361,14 @@ const responseData = prepareCheckoutSessionDataWorkflow.runAsStep({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(responseData)
|
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
|
### 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",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 1,
|
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",
|
type: "when",
|
||||||
condition: "!!data.carts[0].customer",
|
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: 1,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 2,
|
depth: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
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: 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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
transform,
|
transform,
|
||||||
when,
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} 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 { PromotionActions } from "@medusajs/framework/utils"
|
||||||
import { validateTierPromotionStep } from "./steps/validate-tier-promotion"
|
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
|
// Check if customer exists and has tier
|
||||||
const validationResult = when({ carts }, (data) => !!data.carts[0].customer).then(() => {
|
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)
|
return new WorkflowResponse(void 0)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -3250,8 +3279,10 @@ The workflow receives the cart's ID as input.
|
|||||||
In the workflow, you:
|
In the workflow, you:
|
||||||
|
|
||||||
- Retrieve the cart details using `useQueryGraphStep`.
|
- 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`.
|
- 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`.
|
- 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
|
### b. Cart Updated Subscriber
|
||||||
|
|
||||||
|
|||||||
@@ -1278,48 +1278,62 @@ The workflow will have the following steps:
|
|||||||
type: "step",
|
type: "step",
|
||||||
name: "validateCustomerExistsStep",
|
name: "validateCustomerExistsStep",
|
||||||
description: "Validate that the customer is registered.",
|
description: "Validate that the customer is registered.",
|
||||||
depth: 1,
|
depth: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "getCartLoyaltyPromoStep",
|
name: "getCartLoyaltyPromoStep",
|
||||||
description: "Retrieve the cart's loyalty promotion.",
|
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",
|
type: "step",
|
||||||
name: "getCartLoyaltyPromoAmountStep",
|
name: "getCartLoyaltyPromoAmountStep",
|
||||||
description: "Get the amount to be discounted based on the loyalty points.",
|
description: "Get the amount to be discounted based on the loyalty points.",
|
||||||
depth: 1,
|
depth: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "createPromotionsStep",
|
name: "createPromotionsStep",
|
||||||
description: "Create a new loyalty promotion for the cart.",
|
description: "Create a new loyalty promotion for the cart.",
|
||||||
link: "/references/medusa-workflows/steps/createPromotionsStep",
|
link: "/references/medusa-workflows/steps/createPromotionsStep",
|
||||||
depth: 1,
|
depth: 6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
name: "updateCartPromotionsWorkflow",
|
name: "updateCartPromotionsWorkflow",
|
||||||
description: "Update the cart's promotions with the new loyalty promotion.",
|
description: "Update the cart's promotions with the new loyalty promotion.",
|
||||||
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
|
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
|
||||||
depth: 1,
|
depth: 7,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "updateCartsStep",
|
name: "updateCartsStep",
|
||||||
description: "Update the cart to store the ID of the loyalty promotion in the metadata.",
|
description: "Update the cart to store the ID of the loyalty promotion in the metadata.",
|
||||||
link: "/references/medusa-workflows/steps/updateCartsStep",
|
link: "/references/medusa-workflows/steps/updateCartsStep",
|
||||||
depth: 1,
|
depth: 8,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the cart's details again.",
|
description: "Retrieve the cart's details again.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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:
|
To create the workflow, create the file `src/workflows/apply-loyalty-on-cart.ts` with the following content:
|
||||||
|
|
||||||
export const applyLoyaltyOnCartWorkflowHighlights = [
|
export const applyLoyaltyOnCartWorkflowHighlights = [
|
||||||
["44", "useQueryGraphStep", "Retrieve the cart's details."],
|
["46", "useQueryGraphStep", "Retrieve the cart's details."],
|
||||||
["55", "validateCustomerExistsStep", "Validate that the customer is registered."],
|
["57", "validateCustomerExistsStep", "Validate that the customer is registered."],
|
||||||
["59", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
|
["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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
createPromotionsStep,
|
createPromotionsStep,
|
||||||
|
releaseLockStep,
|
||||||
updateCartPromotionsWorkflow,
|
updateCartPromotionsWorkflow,
|
||||||
updateCartsStep,
|
updateCartsStep,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
@@ -1469,6 +1486,12 @@ export const applyLoyaltyOnCartWorkflow = createWorkflow(
|
|||||||
throwErrorOn: "found",
|
throwErrorOn: "found",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
const amount = getCartLoyaltyPromoAmountStep({
|
const amount = getCartLoyaltyPromoAmountStep({
|
||||||
cart: carts[0],
|
cart: carts[0],
|
||||||
} as unknown as GetCartLoyaltyPromoAmountStepInput)
|
} 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.
|
- 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`.
|
- 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.
|
- 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`.
|
- 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.
|
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."],
|
["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."],
|
["29", "updateCartsStep", "Update the cart to store the ID of the loyalty promotion in the metadata."],
|
||||||
["37", "useQueryGraphStep", "Retrieve the cart's details again."],
|
["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}
|
```ts title="src/workflows/apply-loyalty-on-cart.ts" highlights={createLoyaltyPromoStepHighlights}
|
||||||
@@ -1609,6 +1634,10 @@ const { data: updatedCarts } = useQueryGraphStep({
|
|||||||
filters: { id: input.cart_id },
|
filters: { id: input.cart_id },
|
||||||
}).config({ name: "retrieve-cart" })
|
}).config({ name: "retrieve-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(updatedCarts[0])
|
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 promotions with the new loyalty promotion using the `updateCartPromotionsWorkflow` workflow.
|
||||||
- Update the cart's metadata with the loyalty promotion ID using the `updateCartsStep`.
|
- 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.
|
- 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.
|
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",
|
type: "step",
|
||||||
name: "getCartLoyaltyPromoStep",
|
name: "getCartLoyaltyPromoStep",
|
||||||
description: "Retrieve the cart's loyalty promotion.",
|
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",
|
type: "workflow",
|
||||||
name: "updateCartPromotionsWorkflow",
|
name: "updateCartPromotionsWorkflow",
|
||||||
description: "Update the cart's promotions to remove the loyalty promotion.",
|
description: "Update the cart's promotions to remove the loyalty promotion.",
|
||||||
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
|
link: "/references/medusa-workflows/updateCartPromotionsWorkflow",
|
||||||
depth: 1,
|
depth: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "updateCartsStep",
|
name: "updateCartsStep",
|
||||||
description: "Update the cart to remove the loyalty promotion ID from the metadata.",
|
description: "Update the cart to remove the loyalty promotion ID from the metadata.",
|
||||||
link: "/references/medusa-workflows/steps/updateCartsStep",
|
link: "/references/medusa-workflows/steps/updateCartsStep",
|
||||||
depth: 1,
|
depth: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "updatePromotionsStep",
|
name: "updatePromotionsStep",
|
||||||
description: "Deactivate the loyalty promotion.",
|
description: "Deactivate the loyalty promotion.",
|
||||||
link: "/references/medusa-workflows/steps/updatePromotionsStep",
|
link: "/references/medusa-workflows/steps/updatePromotionsStep",
|
||||||
depth: 1,
|
depth: 6,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the cart's details again.",
|
description: "Retrieve the cart's details again.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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:
|
To create the workflow, create the file `src/workflows/remove-loyalty-from-cart.ts` with the following content:
|
||||||
|
|
||||||
export const removeLoyaltyFromCartWorkflowHighlights = [
|
export const removeLoyaltyFromCartWorkflowHighlights = [
|
||||||
["35", "useQueryGraphStep", "Retrieve the cart's details."],
|
["37", "useQueryGraphStep", "Retrieve the cart's details."],
|
||||||
["43", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
|
["48", "getCartLoyaltyPromoStep", "Retrieve the cart's loyalty promotion."],
|
||||||
["48", "updateCartPromotionsWorkflow", "Update the cart's promotions to remove the loyalty promotion."],
|
["53", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
|
||||||
["56", "transform", "Prepare the new metadata to remove the loyalty promotion ID."],
|
["59", "updateCartPromotionsWorkflow", "Update the cart's promotions to remove the loyalty promotion."],
|
||||||
["67", "updateCartsStep", "Update the cart to remove the loyalty promotion ID from the metadata."],
|
["67", "transform", "Prepare the new metadata to remove the loyalty promotion ID."],
|
||||||
["74", "updatePromotionsStep", "Deactivate the loyalty promotion."],
|
["78", "updateCartsStep", "Update the cart to remove the loyalty promotion ID from the metadata."],
|
||||||
["82", "useQueryGraphStep", "Retrieve the cart's details again."],
|
["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}
|
```ts title="src/workflows/remove-loyalty-from-cart.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={removeLoyaltyFromCartWorkflowHighlights}
|
||||||
@@ -1819,6 +1865,8 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
updateCartPromotionsWorkflow,
|
updateCartPromotionsWorkflow,
|
||||||
updateCartsStep,
|
updateCartsStep,
|
||||||
@@ -1853,6 +1901,9 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
|
|||||||
filters: {
|
filters: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
},
|
},
|
||||||
|
options: {
|
||||||
|
throwIfKeyNotFound: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loyaltyPromo = getCartLoyaltyPromoStep({
|
const loyaltyPromo = getCartLoyaltyPromoStep({
|
||||||
@@ -1860,6 +1911,12 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
|
|||||||
throwErrorOn: "not-found",
|
throwErrorOn: "not-found",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
updateCartPromotionsWorkflow.runAsStep({
|
updateCartPromotionsWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
cart_id: input.cart_id,
|
cart_id: input.cart_id,
|
||||||
@@ -1900,6 +1957,10 @@ export const removeLoyaltyFromCartWorkflow = createWorkflow(
|
|||||||
filters: { id: input.cart_id },
|
filters: { id: input.cart_id },
|
||||||
}).config({ name: "retrieve-cart" })
|
}).config({ name: "retrieve-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(updatedCarts[0])
|
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.
|
- 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.
|
- 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.
|
- 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.
|
- 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`.
|
- Update the cart's metadata with the new metadata using the `updateCartsStep`.
|
||||||
- Deactivate the loyalty promotion using the `updatePromotionsStep`.
|
- 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.
|
- 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.
|
- Return the cart's details in a `WorkflowResponse` instance.
|
||||||
|
|
||||||
### Create the API Route
|
### Create the API Route
|
||||||
|
|||||||
@@ -1576,29 +1576,43 @@ The workflow that completes a cart with pre-order items has the following steps:
|
|||||||
workflow={{
|
workflow={{
|
||||||
name: "completeCartPreorderWorkflow",
|
name: "completeCartPreorderWorkflow",
|
||||||
steps: [
|
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",
|
type: "workflow",
|
||||||
name: "completeCartWorkflow",
|
name: "completeCartWorkflow",
|
||||||
description: "Complete the cart with pre-order items.",
|
description: "Complete the cart with pre-order items.",
|
||||||
link: "/references/medusa-workflows/completeCartWorkflow",
|
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",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve all line items in the cart.",
|
description: "Retrieve all line items in the cart.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 2,
|
depth: 4,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "retrievePreorderItemIdsStep",
|
name: "retrievePreorderItemIdsStep",
|
||||||
description: "Retrieve the IDs of pre-order variants in the cart.",
|
description: "Retrieve the IDs of pre-order variants in the cart.",
|
||||||
depth: 3,
|
depth: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
type: "when",
|
||||||
condition: "preorderItemIds.length > 0",
|
condition: "preorders.length === 0 && preorderItemIds.length > 0",
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
@@ -1607,15 +1621,22 @@ The workflow that completes a cart with pre-order items has the following steps:
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
depth: 4
|
depth: 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the created order.",
|
description: "Retrieve the created order.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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:
|
Create the file `src/workflows/complete-cart-preorder.ts` with the following content:
|
||||||
|
|
||||||
export const completeCartPreorderWorkflowHighlights = [
|
export const completeCartPreorderWorkflowHighlights = [
|
||||||
["13", "completeCartWorkflow", "Complete the cart and place the order."],
|
["25", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
|
||||||
["19", "useQueryGraphStep", "Retrieve all line items in the cart."],
|
["30", "completeCartWorkflow", "Complete the cart and place the order."],
|
||||||
["30", "retrievePreorderItemIdsStep", "Retrieve the IDs of pre-order variants in the cart."],
|
["36", "useQueryGraphStep", "Retrieve existing preorders of the order for idempotency."],
|
||||||
["34", "when", "Check if there are pre-order items in the cart."],
|
["46", "useQueryGraphStep", "Retrieve all line items in the cart."],
|
||||||
["38", "createPreordersStep", "Create pre-ordersz for the pre-order items in the cart."],
|
["57", "retrievePreorderItemIdsStep", "Retrieve the IDs of pre-order variants in the cart."],
|
||||||
["44", "useQueryGraphStep", "Retrieve the created order."],
|
["61", "when", "Check that there are no existing preorders and that there are pre-order items in the cart."],
|
||||||
["62", "order", "Return the created Medusa order."],
|
["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}
|
```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 {
|
||||||
import { completeCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
|
createWorkflow,
|
||||||
import { retrievePreorderItemIdsStep, RetrievePreorderItemIdsStepInput } from "./steps/retrieve-preorder-items"
|
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"
|
import { createPreordersStep } from "./steps/create-preorders"
|
||||||
|
|
||||||
type WorkflowInput = {
|
type WorkflowInput = {
|
||||||
@@ -1745,12 +1781,27 @@ type WorkflowInput = {
|
|||||||
export const completeCartPreorderWorkflow = createWorkflow(
|
export const completeCartPreorderWorkflow = createWorkflow(
|
||||||
"complete-cart-preorder",
|
"complete-cart-preorder",
|
||||||
(input: WorkflowInput) => {
|
(input: WorkflowInput) => {
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
const { id } = completeCartWorkflow.runAsStep({
|
const { id } = completeCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: preorders } = useQueryGraphStep({
|
||||||
|
entity: "preorder",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
order_id: id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const { data: line_items } = useQueryGraphStep({
|
const { data: line_items } = useQueryGraphStep({
|
||||||
entity: "line_item",
|
entity: "line_item",
|
||||||
fields: [
|
fields: [
|
||||||
@@ -1760,15 +1811,16 @@ export const completeCartPreorderWorkflow = createWorkflow(
|
|||||||
filters: {
|
filters: {
|
||||||
cart_id: input.cart_id,
|
cart_id: input.cart_id,
|
||||||
},
|
},
|
||||||
})
|
}).config({ name: "retrieve-line-items" })
|
||||||
|
|
||||||
const preorderItemIds = retrievePreorderItemIdsStep({
|
const preorderItemIds = retrievePreorderItemIdsStep({
|
||||||
line_items,
|
line_items,
|
||||||
} as unknown as RetrievePreorderItemIdsStepInput)
|
} as unknown as RetrievePreorderItemIdsStepInput)
|
||||||
|
|
||||||
when({
|
when({
|
||||||
|
preorders,
|
||||||
preorderItemIds,
|
preorderItemIds,
|
||||||
}, (data) => data.preorderItemIds.length > 0)
|
}, (data) => data.preorders.length === 0 && data.preorderItemIds.length > 0)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
createPreordersStep({
|
createPreordersStep({
|
||||||
preorder_variant_ids: preorderItemIds,
|
preorder_variant_ids: preorderItemIds,
|
||||||
@@ -1793,6 +1845,10 @@ export const completeCartPreorderWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "retrieve-order" })
|
}).config({ name: "retrieve-order" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
order: orders[0],
|
order: orders[0],
|
||||||
|
|
||||||
@@ -1805,13 +1861,17 @@ The workflow receives the cart ID as input.
|
|||||||
|
|
||||||
In the workflow, you:
|
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.
|
1. Acquire a lock on the cart using the [acquireLockStep](/references/medusa-workflows/steps/acquireLockStep) to prevent concurrent modifications.
|
||||||
2. Retrieve all line items in the cart using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
|
2. Complete the cart using the [completeCartWorkflow](/references/medusa-workflows/completeCartWorkflow) as a step. This is Medusa's cart completion logic.
|
||||||
3. Retrieve the IDs of the pre-order variants in the cart using the `retrievePreorderItemIdsStep`.
|
3. Retrieve existing pre-orders of the created order using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
|
||||||
4. Use [when-then](!docs!/learn/fundamentals/workflows/conditions) to check if there are pre-order items in the cart.
|
- This is essential for idempotency, ensuring that pre-orders are not created multiple times if the workflow is retried.
|
||||||
- If so, you create `Preorder` records for the pre-order items using the `createPreordersStep`.
|
4. Retrieve all line items in the cart using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
|
||||||
5. Retrieve the created order using the [useQueryGraphStep](/references/helper-steps/useQueryGraphStep).
|
5. Retrieve the IDs of the pre-order variants in the cart using the `retrievePreorderItemIdsStep`.
|
||||||
6. Return the created Medusa order.
|
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
|
### 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",
|
description: "Validates the product builder configuration",
|
||||||
depth: 1,
|
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",
|
type: "step",
|
||||||
name: "addToCartWorkflow",
|
name: "addToCartWorkflow",
|
||||||
description: "Adds the product to the cart.",
|
description: "Adds the product to the cart.",
|
||||||
depth: 2
|
depth: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Get cart with items details.",
|
description: "Get cart with items details.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 3
|
depth: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "when",
|
type: "when",
|
||||||
@@ -4353,14 +4360,21 @@ The workflow will have the following steps:
|
|||||||
depth: 2
|
depth: 2
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
depth: 4
|
depth: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Get updated cart details.",
|
description: "Get updated cart details.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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:
|
Create the file `src/workflows/add-product-builder-to-cart.ts` with the following content:
|
||||||
|
|
||||||
export const addToCartWorkflowHighlights = [
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
transform,
|
transform,
|
||||||
when,
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
|
import {
|
||||||
import { validateProductBuilderConfigurationStep } from "./steps/validate-product-builder-configuration"
|
addToCartWorkflow,
|
||||||
|
updateLineItemInCartWorkflow,
|
||||||
|
useQueryGraphStep,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
|
import {
|
||||||
|
validateProductBuilderConfigurationStep,
|
||||||
|
} from "./steps/validate-product-builder-configuration"
|
||||||
|
|
||||||
type AddProductBuilderToCartInput = {
|
type AddProductBuilderToCartInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -4565,6 +4588,12 @@ export const addProductBuilderToCartWorkflow = createWorkflow(
|
|||||||
addon_variants: input.addon_variants,
|
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
|
// 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.
|
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:
|
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" })
|
}).config({ name: "get-final-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
cart: updatedCart[0],
|
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
|
### 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",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 1
|
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",
|
type: "workflow",
|
||||||
name: "deleteLineItemsWorkflow",
|
name: "deleteLineItemsWorkflow",
|
||||||
description: "Delete line items from the cart.",
|
description: "Delete line items from the cart.",
|
||||||
link: "/references/medusa-workflows/deleteLineItemsWorkflow",
|
link: "/references/medusa-workflows/deleteLineItemsWorkflow",
|
||||||
depth: 2,
|
depth: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the updated cart details.",
|
description: "Retrieve the updated cart details.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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
|
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:
|
Create the file `src/workflows/remove-product-builder-from-cart.ts` with the following content:
|
||||||
|
|
||||||
export const removeProductBuilderFromCartWorkflowHighlights = [
|
export const removeProductBuilderFromCartWorkflowHighlights = [
|
||||||
["17", "carts", "Retrieve cart."],
|
["22", "carts", "Retrieve cart."],
|
||||||
["29", "itemsToRemove", "Identify items to remove."],
|
["33", "acquireLockStep", "Acquire lock on cart."],
|
||||||
["43", "relatedItems", "Identify addon items to remove."],
|
["40", "itemsToRemove", "Identify items to remove."],
|
||||||
["60", "deleteLineItemsWorkflow", "Delete line items from cart."],
|
["51", "relatedItems", "Identify addon items to remove."],
|
||||||
["65", "updatedCart", "Retrieve updated cart."]
|
["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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
transform,
|
transform,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} 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 = {
|
type RemoveProductBuilderFromCartInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -5763,7 +5817,7 @@ export const removeProductBuilderFromCartWorkflow = createWorkflow(
|
|||||||
// Step 1: Get current cart with all items
|
// Step 1: Get current cart with all items
|
||||||
const { data: carts } = useQueryGraphStep({
|
const { data: carts } = useQueryGraphStep({
|
||||||
entity: "cart",
|
entity: "cart",
|
||||||
fields: ["*", "items.*", "items.metadata"],
|
fields: ["*", "items.*"],
|
||||||
filters: {
|
filters: {
|
||||||
id: input.cart_id,
|
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
|
// Step 2: Remove line item and its addons
|
||||||
const itemsToRemove = transform({
|
const itemsToRemove = transform({
|
||||||
input,
|
input,
|
||||||
carts,
|
currentCart: carts,
|
||||||
}, (data) => {
|
}, (data) => {
|
||||||
const cart = data.carts[0]
|
const cart = data.currentCart[0]
|
||||||
const targetLineItem = cart.items.find(
|
const targetLineItem = cart.items.find((item: any) => item.id === data.input.line_item_id)
|
||||||
(item: any) => item.id === data.input.line_item_id
|
|
||||||
)
|
|
||||||
const lineItemIdsToRemove = [data.input.line_item_id]
|
const lineItemIdsToRemove = [data.input.line_item_id]
|
||||||
const isBuilderItem =
|
const isBuilderItem = targetLineItem?.metadata?.is_builder_main_product === true
|
||||||
targetLineItem?.metadata?.is_builder_main_product === true
|
|
||||||
|
|
||||||
if (targetLineItem && isBuilderItem) {
|
if (targetLineItem && isBuilderItem) {
|
||||||
// Find all related addon items
|
// Find all related addon items
|
||||||
@@ -5820,6 +5877,10 @@ export const removeProductBuilderFromCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "get-updated-cart" })
|
}).config({ name: "get-updated-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
cart: updatedCart[0],
|
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:
|
In the workflow, you:
|
||||||
|
|
||||||
|
- Acquire a lock on the cart to prevent concurrent modifications.
|
||||||
- Retrieve the cart details with its items.
|
- Retrieve the cart details with its items.
|
||||||
- Prepare the line items to remove by identifying the main product and its related addons.
|
- Prepare the line items to remove by identifying the main product and its related addons.
|
||||||
- Remove the line items from the cart.
|
- Remove the line items from the cart.
|
||||||
- Retrieve the updated cart details.
|
- Retrieve the updated cart details.
|
||||||
|
- Release the lock on the cart.
|
||||||
|
|
||||||
You return the cart details in the response.
|
You return the cart details in the response.
|
||||||
|
|
||||||
|
|||||||
@@ -2242,19 +2242,33 @@ The workflow will have the following steps:
|
|||||||
],
|
],
|
||||||
depth: 3
|
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",
|
type: "workflow",
|
||||||
name: "addToCartWorkflow",
|
name: "addToCartWorkflow",
|
||||||
description: "Add the product to the cart.",
|
description: "Add the product to the cart.",
|
||||||
link: "/references/medusa-workflows/addToCartWorkflow",
|
link: "/references/medusa-workflows/addToCartWorkflow",
|
||||||
depth: 4,
|
depth: 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve updated cart details.",
|
description: "Retrieve updated cart details.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
transform,
|
transform,
|
||||||
when,
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} 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 { 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 = {
|
type AddToCartWorkflowInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -2500,7 +2522,7 @@ export const addToCartWithRentalWorkflow = createWorkflow(
|
|||||||
options: {
|
options: {
|
||||||
throwIfKeyNotFound: true,
|
throwIfKeyNotFound: true,
|
||||||
},
|
},
|
||||||
}).config({ name: "retrieve-cart" })
|
})
|
||||||
|
|
||||||
const { data: variants } = useQueryGraphStep({
|
const { data: variants } = useQueryGraphStep({
|
||||||
entity: "product_variant",
|
entity: "product_variant",
|
||||||
@@ -2536,6 +2558,12 @@ export const addToCartWithRentalWorkflow = createWorkflow(
|
|||||||
} as unknown as ValidateRentalCartItemInput)
|
} as unknown as ValidateRentalCartItemInput)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
const itemToAdd = transform({
|
const itemToAdd = transform({
|
||||||
input,
|
input,
|
||||||
rentalData,
|
rentalData,
|
||||||
@@ -2574,6 +2602,10 @@ export const addToCartWithRentalWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "refetch-cart" })
|
}).config({ name: "refetch-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
cart: updatedCart[0],
|
cart: updatedCart[0],
|
||||||
})
|
})
|
||||||
@@ -2588,11 +2620,15 @@ In the workflow, you:
|
|||||||
1. Retrieve the cart details using the `useQueryGraphStep`.
|
1. Retrieve the cart details using the `useQueryGraphStep`.
|
||||||
2. Retrieve the product variant 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.
|
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.
|
- 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.
|
- 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. Add the item to the cart using the existing `addToCartWorkflow`.
|
||||||
6. Retrieve the updated cart details and return them in the workflow response.
|
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
|
### 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",
|
link: "/references/medusa-workflows/steps/acquireLockStep",
|
||||||
depth: 2
|
depth: 2
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "step",
|
|
||||||
name: "validateRentalStep",
|
|
||||||
description: "Validate rental items in the cart.",
|
|
||||||
depth: 3
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
name: "completeCartWorkflow",
|
name: "completeCartWorkflow",
|
||||||
description: "Complete the cart and create the order.",
|
description: "Complete the cart and create the order.",
|
||||||
link: "/references/medusa-workflows/completeCartWorkflow",
|
link: "/references/medusa-workflows/completeCartWorkflow",
|
||||||
depth: 4
|
depth: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve order details.",
|
description: "Retrieve order details.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 5
|
depth: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "createRentalsForOrderStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Create rental records for rental items in the order.",
|
description: "Retrieve existing rentals for the order to ensure idempotency.",
|
||||||
depth: 6
|
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",
|
type: "step",
|
||||||
@@ -2871,6 +2921,7 @@ import { InferTypeOf } from "@medusajs/framework/types"
|
|||||||
import { RentalConfiguration } from "../../modules/rental/models/rental-configuration"
|
import { RentalConfiguration } from "../../modules/rental/models/rental-configuration"
|
||||||
import hasCartOverlap from "../../utils/has-cart-overlap"
|
import hasCartOverlap from "../../utils/has-cart-overlap"
|
||||||
import validateRentalDates from "../../utils/validate-rental-dates"
|
import validateRentalDates from "../../utils/validate-rental-dates"
|
||||||
|
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
|
||||||
|
|
||||||
export type ValidateRentalInput = {
|
export type ValidateRentalInput = {
|
||||||
rental_items: {
|
rental_items: {
|
||||||
@@ -2881,12 +2932,13 @@ export type ValidateRentalInput = {
|
|||||||
rental_start_date: Date
|
rental_start_date: Date
|
||||||
rental_end_date: Date
|
rental_end_date: Date
|
||||||
rental_days: number
|
rental_days: number
|
||||||
|
order_id: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateRentalStep = createStep(
|
export const validateRentalStep = createStep(
|
||||||
"validate-rental",
|
"validate-rental",
|
||||||
async ({ rental_items }: ValidateRentalInput, { container }) => {
|
async ({ rental_items, order_id }: ValidateRentalInput, { container }) => {
|
||||||
const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)
|
const rentalModuleService: RentalModuleService = container.resolve(RENTAL_MODULE)
|
||||||
|
|
||||||
for (let i = 0; i < rental_items.length; i++) {
|
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.
|
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
|
#### createRentalsForOrderStep
|
||||||
|
|
||||||
The `createRentalsForOrderStep` creates rental records for rental items in the order after it has been created.
|
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:
|
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 {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
completeCartWorkflow,
|
|
||||||
useQueryGraphStep,
|
|
||||||
acquireLockStep,
|
acquireLockStep,
|
||||||
releaseLockStep,
|
completeCartWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
import {
|
import {
|
||||||
ValidateRentalInput,
|
ValidateRentalInput,
|
||||||
@@ -3139,18 +3205,12 @@ export const createRentalsWorkflow = createWorkflow(
|
|||||||
return rentalItemsList
|
return rentalItemsList
|
||||||
})
|
})
|
||||||
|
|
||||||
const lockKey = transform({
|
|
||||||
cart_id,
|
|
||||||
}, (data) => `cart_rentals_creation_${data.cart_id}`)
|
|
||||||
|
|
||||||
acquireLockStep({
|
acquireLockStep({
|
||||||
key: lockKey,
|
key: cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
validateRentalStep({
|
|
||||||
rental_items: rentalItems,
|
|
||||||
} as unknown as ValidateRentalInput)
|
|
||||||
|
|
||||||
const order = completeCartWorkflow.runAsStep({
|
const order = completeCartWorkflow.runAsStep({
|
||||||
input: { id: cart_id },
|
input: { id: cart_id },
|
||||||
})
|
})
|
||||||
@@ -3169,12 +3229,30 @@ export const createRentalsWorkflow = createWorkflow(
|
|||||||
options: { throwIfKeyNotFound: true },
|
options: { throwIfKeyNotFound: true },
|
||||||
}).config({ name: "retrieve-order" })
|
}).config({ name: "retrieve-order" })
|
||||||
|
|
||||||
createRentalsForOrderStep({
|
const { data: rentals } = useQueryGraphStep({
|
||||||
order: orders[0],
|
entity: "rental",
|
||||||
} as unknown as CreateRentalsForOrderInput)
|
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({
|
releaseLockStep({
|
||||||
key: lockKey,
|
key: cart_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -3192,10 +3270,13 @@ In the workflow, you:
|
|||||||
1. Retrieve the cart details using the `useQueryGraphStep`.
|
1. Retrieve the cart details using the `useQueryGraphStep`.
|
||||||
2. Extract the rental items from the cart.
|
2. Extract the rental items from the cart.
|
||||||
3. Acquire a lock on the cart to prevent race conditions.
|
3. Acquire a lock on the cart to prevent race conditions.
|
||||||
4. Validate the rental items using the `validateRentalStep`.
|
4. Complete the cart and create the order using the existing `completeCartWorkflow`.
|
||||||
5. Complete the cart and create the order using the existing `completeCartWorkflow`.
|
5. Retrieve the created order details using the `useQueryGraphStep`.
|
||||||
6. Retrieve the created order details using the `useQueryGraphStep`.
|
6. Retrieve existing rentals for the order to ensure idempotency.
|
||||||
7. Create rental records for the rental items in the order using the `createRentalsForOrderStep`.
|
- 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.
|
8. Release the lock on the cart.
|
||||||
9. Return the created order in the workflow response.
|
9. Return the created order in the workflow response.
|
||||||
|
|
||||||
|
|||||||
@@ -1752,21 +1752,35 @@ The add-to-cart workflow for bundled products has the following steps:
|
|||||||
name: "prepareBundleCartDataStep",
|
name: "prepareBundleCartDataStep",
|
||||||
type: "step",
|
type: "step",
|
||||||
description: "Validate and prepare the items to be added to the cart.",
|
description: "Validate and prepare the items to be added to the cart.",
|
||||||
depth: 1,
|
depth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "acquireLockStep",
|
||||||
|
type: "step",
|
||||||
|
description: "Acquire a lock on the cart to prevent concurrent modifications",
|
||||||
|
depth: 3,
|
||||||
|
link: "/references/medusa-workflows/steps/acquireLockStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "addToCartWorkflow",
|
name: "addToCartWorkflow",
|
||||||
type: "step",
|
type: "step",
|
||||||
description: "Add the items in the bundle to the cart.",
|
description: "Add the items in the bundle to the cart.",
|
||||||
depth: 1,
|
depth: 4,
|
||||||
link: "/references/medusa-workflows/addToCartWorkflow"
|
link: "/references/medusa-workflows/addToCartWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
type: "step",
|
type: "step",
|
||||||
description: "Retrieve the details of the cart.",
|
description: "Retrieve the details of the cart.",
|
||||||
depth: 1,
|
depth: 5,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "releaseLockStep",
|
||||||
|
type: "step",
|
||||||
|
description: "Release the lock on the cart.",
|
||||||
|
depth: 6,
|
||||||
|
link: "/references/medusa-workflows/steps/releaseLockStep"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
@@ -1889,20 +1903,24 @@ You can now create the workflow with the custom add-to-cart logic.
|
|||||||
To create the workflow, create the file `src/workflows/add-bundle-to-cart.ts` with the following content:
|
To create the workflow, create the file `src/workflows/add-bundle-to-cart.ts` with the following content:
|
||||||
|
|
||||||
export const addBundleToCartWorkflowHighlights = [
|
export const addBundleToCartWorkflowHighlights = [
|
||||||
["28", "useQueryGraphStep", "Retrieve the bundle, its items, and their products and variants."],
|
["30", "useQueryGraphStep", "Retrieve the bundle, its items, and their products and variants."],
|
||||||
["44", "prepareBundleCartDataStep", "Validate and prepare the items to be added to the cart."],
|
["46", "prepareBundleCartDataStep", "Validate and prepare the items to be added to the cart."],
|
||||||
["50", "addToCartWorkflow", "Add the items in the bundle to the cart."],
|
["52", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
|
||||||
["57", "useQueryGraphStep", "Retrieve the updated cart."],
|
["58", "addToCartWorkflow", "Add the items in the bundle to the cart."],
|
||||||
|
["65", "useQueryGraphStep", "Retrieve the updated cart."],
|
||||||
|
["71", "releaseLockStep", "Release the lock on the cart."],
|
||||||
]
|
]
|
||||||
|
|
||||||
```ts title="src/workflows/add-bundle-to-cart.ts" highlights={addBundleToCartWorkflowHighlights} collapsibleLines="1-14" expandButtonLabel="Show Imports"
|
```ts title="src/workflows/add-bundle-to-cart.ts" highlights={addBundleToCartWorkflowHighlights} collapsibleLines="1-16" expandButtonLabel="Show Imports"
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
addToCartWorkflow,
|
acquireLockStep,
|
||||||
|
addToCartWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
import {
|
import {
|
||||||
@@ -1945,6 +1963,12 @@ export const addBundleToCartWorkflow = createWorkflow(
|
|||||||
items,
|
items,
|
||||||
} as unknown as PrepareBundleCartDataStepInput)
|
} as unknown as PrepareBundleCartDataStepInput)
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
addToCartWorkflow.runAsStep({
|
addToCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
cart_id,
|
cart_id,
|
||||||
@@ -1958,6 +1982,10 @@ export const addBundleToCartWorkflow = createWorkflow(
|
|||||||
fields: ["id", "items.*"],
|
fields: ["id", "items.*"],
|
||||||
}).config({ name: "refetch-cart" })
|
}).config({ name: "refetch-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(updatedCarts[0])
|
return new WorkflowResponse(updatedCarts[0])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1969,8 +1997,10 @@ In the workflow, you:
|
|||||||
|
|
||||||
- Retrieve the bundle, its items, and their products and variants using the `useQueryGraphStep`.
|
- Retrieve the bundle, its items, and their products and variants using the `useQueryGraphStep`.
|
||||||
- Validate and prepare the items to be added to the cart using the `prepareBundleCartDataStep`.
|
- Validate and prepare the items to be added to the cart using the `prepareBundleCartDataStep`.
|
||||||
|
- Acquire a lock on the cart using the `acquireLockStep`.
|
||||||
- Add the items to the cart using the `addToCartWorkflow`.
|
- Add the items to the cart using the `addToCartWorkflow`.
|
||||||
- Retrieve the updated cart using the `useQueryGraphStep`.
|
- Retrieve the updated cart using the `useQueryGraphStep`.
|
||||||
|
- Release the lock on the cart using the `releaseLockStep`.
|
||||||
|
|
||||||
Finally, you return the updated cart.
|
Finally, you return the updated cart.
|
||||||
|
|
||||||
@@ -2774,20 +2804,34 @@ The workflow has the following steps:
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "acquireLockStep",
|
||||||
|
type: "step",
|
||||||
|
description: "Acquire a lock on the cart to prevent concurrent modifications.",
|
||||||
|
depth: 2,
|
||||||
|
link: "/references/medusa-workflows/steps/acquireLockStep"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "deleteLineItemsWorkflow",
|
name: "deleteLineItemsWorkflow",
|
||||||
type: "workflow",
|
type: "workflow",
|
||||||
description: "Remove the items in the bundle from the cart.",
|
description: "Remove the items in the bundle from the cart.",
|
||||||
depth: 1,
|
depth: 3,
|
||||||
link: "/references/medusa-workflows/deleteLineItemsWorkflow"
|
link: "/references/medusa-workflows/deleteLineItemsWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
type: "step",
|
type: "step",
|
||||||
description: "Retrieve the updated cart.",
|
description: "Retrieve the updated cart.",
|
||||||
depth: 1,
|
depth: 4,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: "releaseLockStep",
|
||||||
|
type: "step",
|
||||||
|
description: "Release the lock on the cart.",
|
||||||
|
depth: 5,
|
||||||
|
link: "/references/medusa-workflows/steps/releaseLockStep"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
hideLegend
|
hideLegend
|
||||||
@@ -2798,20 +2842,24 @@ Medusa provides all these steps and workflows in its `@medusajs/medusa/core-flow
|
|||||||
Create the file `src/workflows/remove-bundle-from-cart.ts` with the following content:
|
Create the file `src/workflows/remove-bundle-from-cart.ts` with the following content:
|
||||||
|
|
||||||
export const removeBundleFromCartWorkflowHighlights = [
|
export const removeBundleFromCartWorkflowHighlights = [
|
||||||
["19", "useQueryGraphStep", "Retrieve the details of the cart and its items."],
|
["21", "useQueryGraphStep", "Retrieve the details of the cart and its items."],
|
||||||
["33", "transform", "Prepare the IDs of the items to remove."],
|
["35", "transform", "Prepare the IDs of the items to remove."],
|
||||||
["42", "deleteLineItemsWorkflow", "Remove the items in the bundle from the cart."],
|
["44", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
|
||||||
["50", "useQueryGraphStep", "Retrieve the updated cart."],
|
["50", "deleteLineItemsWorkflow", "Remove the items in the bundle from the cart."],
|
||||||
|
["58", "useQueryGraphStep", "Retrieve the updated cart."],
|
||||||
|
["69", "releaseLockStep", "Release the lock on the cart."],
|
||||||
]
|
]
|
||||||
|
|
||||||
```ts title="src/workflows/remove-bundle-from-cart.ts" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={removeBundleFromCartWorkflowHighlights}
|
```ts title="src/workflows/remove-bundle-from-cart.ts" collapsibleLines="1-12" expandButtonLabel="Show Imports" highlights={removeBundleFromCartWorkflowHighlights}
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
|
acquireLockStep,
|
||||||
deleteLineItemsWorkflow,
|
deleteLineItemsWorkflow,
|
||||||
|
releaseLockStep,
|
||||||
useQueryGraphStep,
|
useQueryGraphStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
|
|
||||||
@@ -2846,6 +2894,12 @@ export const removeBundleFromCartWorkflow = createWorkflow(
|
|||||||
}).map((item) => item!.id)
|
}).map((item) => item!.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
deleteLineItemsWorkflow.runAsStep({
|
deleteLineItemsWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
cart_id,
|
cart_id,
|
||||||
@@ -2865,6 +2919,10 @@ export const removeBundleFromCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "retrieve-cart" })
|
}).config({ name: "retrieve-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse(updatedCarts[0])
|
return new WorkflowResponse(updatedCarts[0])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -2876,8 +2934,10 @@ In the workflow, you:
|
|||||||
|
|
||||||
- Retrieve the cart and its items using the `useQueryGraphStep`.
|
- Retrieve the cart and its items using the `useQueryGraphStep`.
|
||||||
- Use `transform` to filter the items in the cart and return only the IDs of the items that belong to the bundle.
|
- Use `transform` to filter the items in the cart and return only the IDs of the items that belong to the bundle.
|
||||||
|
- Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
|
||||||
- Remove the items from the cart using the `deleteLineItemsWorkflow`.
|
- Remove the items from the cart using the `deleteLineItemsWorkflow`.
|
||||||
- Retrieve the updated cart using the `useQueryGraphStep`.
|
- Retrieve the updated cart using the `useQueryGraphStep`.
|
||||||
|
- Release the lock on the cart using the `releaseLockStep`.
|
||||||
|
|
||||||
Finally, you return the updated cart.
|
Finally, you return the updated cart.
|
||||||
|
|
||||||
|
|||||||
@@ -1983,6 +1983,8 @@ import {
|
|||||||
createRemoteLinkStep,
|
createRemoteLinkStep,
|
||||||
createOrderFulfillmentWorkflow,
|
createOrderFulfillmentWorkflow,
|
||||||
emitEventStep,
|
emitEventStep,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
import {
|
import {
|
||||||
Modules,
|
Modules,
|
||||||
@@ -1991,6 +1993,7 @@ import createDigitalProductOrderStep, {
|
|||||||
CreateDigitalProductOrderStepInput,
|
CreateDigitalProductOrderStepInput,
|
||||||
} from "./steps/create-digital-product-order"
|
} from "./steps/create-digital-product-order"
|
||||||
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
|
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product"
|
||||||
|
import digitalProductOrderOrderLink from "../../links/digital-product-order"
|
||||||
|
|
||||||
type WorkflowInput = {
|
type WorkflowInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -1999,6 +2002,11 @@ type WorkflowInput = {
|
|||||||
const createDigitalProductOrderWorkflow = createWorkflow(
|
const createDigitalProductOrderWorkflow = createWorkflow(
|
||||||
"create-digital-product-order",
|
"create-digital-product-order",
|
||||||
(input: WorkflowInput) => {
|
(input: WorkflowInput) => {
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 30,
|
||||||
|
ttl: 120,
|
||||||
|
})
|
||||||
const { id } = completeCartWorkflow.runAsStep({
|
const { id } = completeCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
@@ -2022,19 +2030,31 @@ const createDigitalProductOrderWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemsWithDigitalProducts = transform({
|
const { data: existingLinks } = useQueryGraphStep({
|
||||||
orders,
|
entity: digitalProductOrderOrderLink.entryPoint,
|
||||||
},
|
fields: ["digital_product_order.id"],
|
||||||
(data) => {
|
filters: { order_id: id },
|
||||||
return data.orders[0].items?.filter((item) => item?.variant?.digital_product !== undefined)
|
}).config({ name: "retrieve-existing-links" })
|
||||||
|
|
||||||
|
const itemsWithDigitalProducts = transform(
|
||||||
|
{
|
||||||
|
orders,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
return data.orders[0].items?.filter(
|
||||||
|
(item) => item?.variant?.digital_product !== undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const digital_product_order = when(
|
const digital_product_order = when(
|
||||||
"create-digital-product-order-condition",
|
"create-digital-product-order-condition",
|
||||||
itemsWithDigitalProducts,
|
{ itemsWithDigitalProducts, existingLinks },
|
||||||
(itemsWithDigitalProducts) => {
|
(data) => {
|
||||||
return !!itemsWithDigitalProducts?.length
|
return (
|
||||||
|
!!data.itemsWithDigitalProducts?.length &&
|
||||||
|
data.existingLinks.length === 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
).then(() => {
|
).then(() => {
|
||||||
const {
|
const {
|
||||||
@@ -2076,6 +2096,10 @@ const createDigitalProductOrderWorkflow = createWorkflow(
|
|||||||
return digital_product_order
|
return digital_product_order
|
||||||
})
|
})
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
order: orders[0],
|
order: orders[0],
|
||||||
digital_product_order,
|
digital_product_order,
|
||||||
@@ -2088,13 +2112,18 @@ export default createDigitalProductOrderWorkflow
|
|||||||
|
|
||||||
This creates the workflow `createDigitalProductOrderWorkflow`. It runs the following steps:
|
This creates the workflow `createDigitalProductOrderWorkflow`. It runs the following steps:
|
||||||
|
|
||||||
1. `completeCartWorkflow` as a step to create the Medusa order.
|
1. `acquireLockStep` to acquire a lock on the cart ID.
|
||||||
2. `useQueryGraphStep` to retrieve the order’s items with their associated variants and linked digital products.
|
2. `completeCartWorkflow` as a step to create the Medusa order.
|
||||||
3. Use `when` to check whether the order has digital products. If so:
|
3. `useQueryGraphStep` to retrieve the order’s items with their associated variants and linked digital products.
|
||||||
|
4. `useQueryGraphStep` to retrieve any existing links between the digital product order and the Medusa order.
|
||||||
|
- This is necessary to ensure the workflow is idempotent, meaning that if the workflow is executed multiple times for the same cart, it doesn’t create multiple digital product orders.
|
||||||
|
5. Use `transform` to filter the order’s items to only those having digital products.
|
||||||
|
6. Use `when` to check whether the order has digital products and no existing links. If so:
|
||||||
1. Use the `createDigitalProductOrderStep` to create the digital product order.
|
1. Use the `createDigitalProductOrderStep` to create the digital product order.
|
||||||
2. Use the `createRemoteLinkStep` to link the digital product order to the Medusa order.
|
2. Use the `createRemoteLinkStep` to link the digital product order to the Medusa order.
|
||||||
3. Use the `createOrderFulfillmentWorkflow` to create a fulfillment for the digital products in the order.
|
3. Use the `createOrderFulfillmentWorkflow` to create a fulfillment for the digital products in the order.
|
||||||
4. Use the `emitEventStep` to emit a custom event.
|
4. Use the `emitEventStep` to emit a custom event.
|
||||||
|
7. `releaseLockStep` to release the lock on the cart ID.
|
||||||
|
|
||||||
The workflow returns the Medusa order and the digital product order, if created.
|
The workflow returns the Medusa order and the digital product order, if created.
|
||||||
|
|
||||||
|
|||||||
@@ -1281,47 +1281,75 @@ In this step, you’ll create a workflow that’s executed when the customer pla
|
|||||||
depth: 1,
|
depth: 1,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "acquireLockStep",
|
||||||
|
description: "Acquire a lock on the cart to avoid concurrent modifications.",
|
||||||
|
depth: 2,
|
||||||
|
link: "/references/medusa-workflows/steps/acquireLockStep"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "completeCartWorkflow",
|
name: "completeCartWorkflow",
|
||||||
description: "Create the parent order from the cart.",
|
description: "Create the parent order from the cart.",
|
||||||
depth: 1,
|
depth: 3,
|
||||||
link: "/references/medusa-workflows/completeCartWorkflow"
|
link: "/references/medusa-workflows/completeCartWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "groupVendorItemsStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Group the items by their vendor.",
|
description: "Retrieve existing links between the order and variant to ensure idempotency.",
|
||||||
depth: 1,
|
depth: 4,
|
||||||
link: "#groupvendoritemssstep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "workflow",
|
||||||
name: "getOrderDetailWorkflow",
|
name: "getOrderDetailWorkflow",
|
||||||
description: "Retrieve the parent order's details.",
|
description: "Retrieve the parent order's details.",
|
||||||
depth: 1,
|
depth: 5,
|
||||||
link: "/references/medusa-workflows/getOrderDetailWorkflow"
|
link: "/references/medusa-workflows/getOrderDetailWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "when",
|
||||||
name: "createVendorOrdersStep",
|
condition: "existingLinks.length === 0",
|
||||||
description: "Create child orders for each vendor.",
|
steps: [
|
||||||
depth: 1,
|
{
|
||||||
link: "#createvendorordersstep"
|
type: "step",
|
||||||
|
name: "groupVendorItemsStep",
|
||||||
|
description: "Group the items by their vendor.",
|
||||||
|
depth: 4,
|
||||||
|
link: "#groupvendoritemssstep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "createVendorOrdersStep",
|
||||||
|
description: "Create child orders for each vendor.",
|
||||||
|
depth: 6,
|
||||||
|
link: "#createvendorordersstep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "createRemoteLinkStep",
|
||||||
|
description: "Create links between vendors and orders.",
|
||||||
|
depth: 7,
|
||||||
|
link: "/references/helper-steps/createRemoteLinkStep"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
depth: 6
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "createRemoteLinkStep",
|
name: "releaseLockStep",
|
||||||
description: "Create links between vendors and orders.",
|
description: "Release the lock on the cart.",
|
||||||
depth: 1,
|
depth: 7,
|
||||||
link: "/references/helper-steps/createRemoteLinkStep"
|
link: "/references/medusa-workflows/steps/releaseLockStep"
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
hideLegend
|
hideLegend
|
||||||
/>
|
/>
|
||||||
|
|
||||||
You only need to implement the third and fourth steps, as Medusa provides the rest of the steps in its `@medusajs/medusa/core-flows` package.
|
You only need to implement the `groupVendorItemsStep` and `createVendorOrdersStep` steps, as Medusa provides the rest of the steps in its `@medusajs/medusa/core-flows` package.
|
||||||
|
|
||||||
### groupVendorItemsStep
|
### groupVendorItemsStep
|
||||||
|
|
||||||
@@ -1647,17 +1675,22 @@ Now that you have all the necessary steps, you can create the workflow.
|
|||||||
Create the workflow at the file `src/workflows/marketplace/create-vendor-orders/index.ts`:
|
Create the workflow at the file `src/workflows/marketplace/create-vendor-orders/index.ts`:
|
||||||
|
|
||||||
export const createVendorOrdersWorkflowHighlights = [
|
export const createVendorOrdersWorkflowHighlights = [
|
||||||
["21", "useQueryGraphStep", "Retrieve the cart's details."],
|
["25", "useQueryGraphStep", "Retrieve the cart's details."],
|
||||||
["30", "completeCartWorkflow", "Create the parent order from the cart."],
|
["34", "acquireLockStep", "Acquire a lock on the cart to avoid concurrent modifications."],
|
||||||
["36", "groupVendorItemsStep", "Group the items by their vendor."],
|
["40", "completeCartWorkflow", "Create the parent order from the cart."],
|
||||||
["40", "getOrderDetailWorkflow", "Retrieve the parent order's details."],
|
["46", "useQueryGraphStep", "Retrieve existing links between the order and variant to ensure idempotency."],
|
||||||
["61", "createVendorOrdersStep", "Create child orders for each vendor"],
|
["52", "getOrderDetailWorkflow", "Retrieve the parent order's details."],
|
||||||
["66", "createRemoteLinkStep", "Create the links returned by the previous step."]
|
["70", "when", "Check if there are existing links to ensure idempotency."],
|
||||||
|
["76", "groupVendorItemsStep", "Group the items by their vendor."],
|
||||||
|
["83", "createVendorOrdersStep", "Create child orders for each vendor"],
|
||||||
|
["88", "createRemoteLinkStep", "Create the links returned by the previous step."],
|
||||||
|
["93", "releaseLockStep", "Release the lock on the cart."],
|
||||||
]
|
]
|
||||||
|
|
||||||
```ts title="src/workflows/marketplace/create-vendor-orders/index.ts" collapsibleLines="1-13" expandMoreLabel="Show Imports"
|
```ts title="src/workflows/marketplace/create-vendor-orders/index.ts" collapsibleLines="1-17" expandMoreLabel="Show Imports"
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
|
when,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
@@ -1665,9 +1698,12 @@ import {
|
|||||||
createRemoteLinkStep,
|
createRemoteLinkStep,
|
||||||
completeCartWorkflow,
|
completeCartWorkflow,
|
||||||
getOrderDetailWorkflow,
|
getOrderDetailWorkflow,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
} from "@medusajs/medusa/core-flows"
|
} from "@medusajs/medusa/core-flows"
|
||||||
import groupVendorItemsStep from "./steps/group-vendor-items"
|
import groupVendorItemsStep, { GroupVendorItemsStepInput } from "./steps/group-vendor-items"
|
||||||
import createVendorOrdersStep from "./steps/create-vendor-orders"
|
import createVendorOrdersStep from "./steps/create-vendor-orders"
|
||||||
|
import vendorOrderLink from "../../../links/vendor-order"
|
||||||
|
|
||||||
type WorkflowInput = {
|
type WorkflowInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -1685,16 +1721,24 @@ const createVendorOrdersWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
const { id: orderId } = completeCartWorkflow.runAsStep({
|
const { id: orderId } = completeCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
id: carts[0].id,
|
id: carts[0].id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { vendorsItems } = groupVendorItemsStep({
|
const { data: existingLinks } = useQueryGraphStep({
|
||||||
cart: carts[0],
|
entity: vendorOrderLink.entryPoint,
|
||||||
} as unknown as GroupVendorItemsStepInput)
|
fields: ["vendor.id"],
|
||||||
|
filters: { order_id: orderId },
|
||||||
|
}).config({ name: "retrieve-existing-links" })
|
||||||
|
|
||||||
const order = getOrderDetailWorkflow.runAsStep({
|
const order = getOrderDetailWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
@@ -1713,19 +1757,36 @@ const createVendorOrdersWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const vendorOrders = when(
|
||||||
orders: vendorOrders,
|
"create-vendor-order-links",
|
||||||
linkDefs,
|
{ existingLinks },
|
||||||
} = createVendorOrdersStep({
|
(data) => data.existingLinks.length === 0
|
||||||
parentOrder: order,
|
).then(() => {
|
||||||
vendorsItems,
|
|
||||||
|
const { vendorsItems } = groupVendorItemsStep({
|
||||||
|
cart: carts[0],
|
||||||
|
} as unknown as GroupVendorItemsStepInput)
|
||||||
|
|
||||||
|
const {
|
||||||
|
orders: vendorOrders,
|
||||||
|
linkDefs,
|
||||||
|
} = createVendorOrdersStep({
|
||||||
|
parentOrder: order,
|
||||||
|
vendorsItems,
|
||||||
|
})
|
||||||
|
|
||||||
|
createRemoteLinkStep(linkDefs)
|
||||||
|
|
||||||
|
return vendorOrders
|
||||||
})
|
})
|
||||||
|
|
||||||
createRemoteLinkStep(linkDefs)
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
parent_order: order,
|
order,
|
||||||
vendor_orders: vendorOrders,
|
vendorOrders,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -1736,11 +1797,16 @@ export default createVendorOrdersWorkflow
|
|||||||
The workflow receives the cart's ID as an input. In the workflow, you run the following steps:
|
The workflow receives the cart's ID as an input. In the workflow, you run the following steps:
|
||||||
|
|
||||||
1. `useQueryGraphStep` to retrieve the cart's details.
|
1. `useQueryGraphStep` to retrieve the cart's details.
|
||||||
2. `completeCartWorkflow` to complete the cart and create a parent order.
|
2. `acquireLockStep` to acquire a lock on the cart.
|
||||||
3. `groupVendorItemsStep` to group the order's items by their vendor.
|
3. `completeCartWorkflow` to complete the cart and create a parent order.
|
||||||
4. `getOrderDetailWorkflow` to retrieve the parent order's details.
|
4. `useQueryGraphStep` to retrieve existing links between the order and variant to ensure idempotency.
|
||||||
5. `createVendorOrdersStep` to create child orders for each vendor's items.
|
- This is essential, as the workflow might be executed multiple times for the same cart. So, if links already exist, you skip creating vendor orders again.
|
||||||
6. `createRemoteLinkStep` to create the links returned by the previous step.
|
5. `getOrderDetailWorkflow` to retrieve the parent order's details.
|
||||||
|
6. Perform a condition with `when` to check if there are existing links. If not, you run the following steps:
|
||||||
|
- `groupVendorItemsStep` to group the items by their vendor.
|
||||||
|
- `createVendorOrdersStep` to create child orders for each vendor.
|
||||||
|
- `createRemoteLinkStep` to create the links returned by the previous step.
|
||||||
|
7. `releaseLockStep` to release the lock on the cart.
|
||||||
|
|
||||||
You return the parent and vendor orders.
|
You return the parent and vendor orders.
|
||||||
|
|
||||||
|
|||||||
@@ -744,19 +744,33 @@ The custom add-to-cart workflow will have the following steps:
|
|||||||
description: "Get the custom price for the product variant.",
|
description: "Get the custom price for the product variant.",
|
||||||
depth: 2
|
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",
|
type: "workflow",
|
||||||
name: "addToCartWorkflow",
|
name: "addToCartWorkflow",
|
||||||
description: "Add the product variant to the cart with the custom price.",
|
description: "Add the product variant to the cart with the custom price.",
|
||||||
link: "/references/medusa-workflows/addToCartWorkflow",
|
link: "/references/medusa-workflows/addToCartWorkflow",
|
||||||
depth: 3
|
depth: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Get the cart's updated data.",
|
description: "Get the cart's updated data.",
|
||||||
link: "/references/helper-steps/useQueryGraphStep",
|
link: "/references/helper-steps/useQueryGraphStep",
|
||||||
depth: 4
|
depth: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "releaseLockStep",
|
||||||
|
description: "Release the lock on the cart.",
|
||||||
|
link: "/references/medusa-workflows/steps/releaseLockStep",
|
||||||
|
depth: 6
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
@@ -768,15 +782,22 @@ You already have all the necessary steps within the workflow, so you create it r
|
|||||||
Create the file `src/workflows/custom-add-to-cart.ts` with the following content:
|
Create the file `src/workflows/custom-add-to-cart.ts` with the following content:
|
||||||
|
|
||||||
export const customAddToCartWorkflowHighlights = [
|
export const customAddToCartWorkflowHighlights = [
|
||||||
["17", "useQueryGraphStep", "Get the cart's region."],
|
["22", "useQueryGraphStep", "Get the cart's region."],
|
||||||
["27", "getCustomPriceWorkflow", "Get the custom price for the product variant."],
|
["32", "getCustomPriceWorkflow", "Get the custom price for the product variant."],
|
||||||
["47", "addToCartWorkflow", "Add the product variant to the cart with the custom price."],
|
["40", "acquireLockStep", "Acquire a lock on the cart to prevent concurrent modifications."],
|
||||||
["55", "useQueryGraphStep", "Get the updated cart data."]
|
["58", "addToCartWorkflow", "Add the product variant to the cart with the custom price."],
|
||||||
|
["66", "useQueryGraphStep", "Get the updated cart data."],
|
||||||
|
["74", "releaseLockStep", "Release the lock on the cart."]
|
||||||
]
|
]
|
||||||
|
|
||||||
```ts title="src/workflows/custom-add-to-cart.ts" highlights={customAddToCartWorkflowHighlights}
|
```ts title="src/workflows/custom-add-to-cart.ts" highlights={customAddToCartWorkflowHighlights}
|
||||||
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
import { addToCartWorkflow, useQueryGraphStep } from "@medusajs/medusa/core-flows"
|
import {
|
||||||
|
addToCartWorkflow,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
|
useQueryGraphStep,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
import { getCustomPriceWorkflow } from "./get-custom-price"
|
import { getCustomPriceWorkflow } from "./get-custom-price"
|
||||||
|
|
||||||
type CustomAddToCartWorkflowInput = {
|
type CustomAddToCartWorkflowInput = {
|
||||||
@@ -808,6 +829,12 @@ export const customAddToCartWorkflow = createWorkflow(
|
|||||||
metadata: input.item.metadata,
|
metadata: input.item.metadata,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
|
|
||||||
const itemData = transform({
|
const itemData = transform({
|
||||||
item: input.item,
|
item: input.item,
|
||||||
@@ -837,6 +864,10 @@ export const customAddToCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "refetch-cart" })
|
}).config({ name: "refetch-cart" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
cart: updatedCart[0],
|
cart: updatedCart[0],
|
||||||
})
|
})
|
||||||
@@ -850,10 +881,12 @@ In the workflow, you:
|
|||||||
|
|
||||||
1. Retrieve the cart's region using the `useQueryGraphStep`.
|
1. Retrieve the cart's region using the `useQueryGraphStep`.
|
||||||
2. Calculate the custom price using the `getCustomPriceWorkflow` that you created earlier.
|
2. Calculate the custom price using the `getCustomPriceWorkflow` that you created earlier.
|
||||||
3. Prepare the data of the item to add to the cart.
|
3. Acquire a lock on the cart using the `acquireLockStep` to prevent concurrent modifications.
|
||||||
|
4. Prepare the data of the item to add to the cart.
|
||||||
- You use the `transform` function because direct data manipulation isn't allowed in workflows. Refer to the [Data Manipulation](!docs!/learn/fundamentals/workflows/variable-manipulation) guide to learn more.
|
- You use the `transform` function because direct data manipulation isn't allowed in workflows. Refer to the [Data Manipulation](!docs!/learn/fundamentals/workflows/variable-manipulation) guide to learn more.
|
||||||
4. Add the item to the cart using the `addToCartWorkflow`.
|
5. Add the item to the cart using the `addToCartWorkflow`.
|
||||||
5. Refetch the updated cart using the `useQueryGraphStep` to return the cart data in the workflow's response.
|
6. Refetch the updated cart using the `useQueryGraphStep` to return the cart data in the workflow's response.
|
||||||
|
7. Release the lock on the cart using the `releaseLockStep`.
|
||||||
|
|
||||||
### b. Create the Add-to-Cart API Route
|
### b. Create the Add-to-Cart API Route
|
||||||
|
|
||||||
|
|||||||
@@ -732,6 +732,9 @@ export const createSubscriptionWorkflowHighlights = [
|
|||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
|
useQueryGraphStep,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import {
|
import {
|
||||||
createRemoteLinkStep,
|
createRemoteLinkStep,
|
||||||
@@ -754,6 +757,11 @@ type WorkflowInput = {
|
|||||||
const createSubscriptionWorkflow = createWorkflow(
|
const createSubscriptionWorkflow = createWorkflow(
|
||||||
"create-subscription",
|
"create-subscription",
|
||||||
(input: WorkflowInput) => {
|
(input: WorkflowInput) => {
|
||||||
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
const { id } = completeCartWorkflow.runAsStep({
|
const { id } = completeCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
@@ -815,14 +823,34 @@ const createSubscriptionWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { subscription, linkDefs } = createSubscriptionStep({
|
const { data: existingLinks } = useQueryGraphStep({
|
||||||
cart_id: input.cart_id,
|
entity: subscriptionOrderLink.entryPoint,
|
||||||
order_id: orders[0].id,
|
fields: ["subscription.id"],
|
||||||
customer_id: orders[0].customer_id!,
|
filters: { order_id: orders[0].id },
|
||||||
subscription_data: input.subscription_data,
|
}).config({ name: "retrieve-existing-links" })
|
||||||
|
|
||||||
|
const subscription = when(
|
||||||
|
"create-subscription-condition",
|
||||||
|
{ existingLinks },
|
||||||
|
(data) => data.existingLinks.length === 0
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
|
||||||
|
const { subscription, linkDefs } = createSubscriptionStep({
|
||||||
|
cart_id: input.cart_id,
|
||||||
|
order_id: orders[0].id,
|
||||||
|
customer_id: orders[0].customer_id!,
|
||||||
|
subscription_data: input.subscription_data,
|
||||||
|
})
|
||||||
|
|
||||||
|
createRemoteLinkStep(linkDefs)
|
||||||
|
|
||||||
|
return subscription
|
||||||
})
|
})
|
||||||
|
|
||||||
createRemoteLinkStep(linkDefs)
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
subscription: subscription,
|
subscription: subscription,
|
||||||
@@ -836,10 +864,14 @@ export default createSubscriptionWorkflow
|
|||||||
|
|
||||||
This workflow accepts the cart’s ID, along with the subscription details. It executes the following steps:
|
This workflow accepts the cart’s ID, along with the subscription details. It executes the following steps:
|
||||||
|
|
||||||
1. `completeCartWorkflow` from `@medusajs/medusa/core-flows` that completes a cart and creates an order.
|
1. [acquireLockStep](/references/medusa-workflows/steps/acquireLockStep) to acquire a lock on the cart to prevent race conditions.
|
||||||
2. `useQueryGraphStep` from `@medusajs/medusa/core-flows` to retrieve the order's details. [Query](!docs!/learn/fundamentals/module-links/query) is a tool that allows you to retrieve data across modules.
|
2. [completeCartWorkflow](/references/medusa-workflows/completeCartWorkflow) that completes a cart and creates an order.
|
||||||
3. `createSubscriptionStep`, which is the step you created previously.
|
3. [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) to retrieve the order's details. [Query](!docs!/learn/fundamentals/module-links/query) is a tool that allows you to retrieve data across modules.
|
||||||
4. `createRemoteLinkStep` from `@medusajs/medusa/core-flows`, which accepts links to create. These links are in the `linkDefs` array returned by the previous step.
|
4. [useQueryGraphStep](/references/helper-steps/useQueryGraphStep) again to check if a subscription already exists for the order. This is necessary to ensure idempotency in case the workflow is retried.
|
||||||
|
5. Use `when` to check if a subscription already exists for the order. If not, it executes the next steps:
|
||||||
|
1. `createSubscriptionStep`, which is the step you created previously.
|
||||||
|
2. `createRemoteLinkStep` which accepts links to create. These links are in the `linkDefs` array returned by the previous step.
|
||||||
|
6. [releaseLockStep](/references/medusa-workflows/steps/releaseLockStep) to release the lock on the cart.
|
||||||
|
|
||||||
The workflow returns the created subscription and order.
|
The workflow returns the created subscription and order.
|
||||||
|
|
||||||
@@ -918,7 +950,7 @@ export const POST = async (
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
type: "order",
|
type: "order",
|
||||||
...result,
|
order: result.order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -927,7 +959,7 @@ Since the file exports a `POST` function, you're exposing a `POST` API route at
|
|||||||
|
|
||||||
In the route handler function, you retrieve the cart to access it's `metadata` property. If the subscription details aren't stored there, you throw an error.
|
In the route handler function, you retrieve the cart to access it's `metadata` property. If the subscription details aren't stored there, you throw an error.
|
||||||
|
|
||||||
Then, you use the `createSubscriptionWorkflow` you created to create the order, and return the created order and subscription in the response.
|
Then, you use the `createSubscriptionWorkflow` you created to create the order, and return the created order in the response.
|
||||||
|
|
||||||
In the next step, you'll customize the Next.js Starter Storefront, allowing you to test out the subscription feature.
|
In the next step, you'll customize the Next.js Starter Storefront, allowing you to test out the subscription feature.
|
||||||
|
|
||||||
@@ -965,6 +997,7 @@ In this step, you'll customize the checkout flow in the [Next.js Starter storefr
|
|||||||
|
|
||||||
1. Add a subscription step to the checkout flow.
|
1. Add a subscription step to the checkout flow.
|
||||||
2. Pass the additional data that Stripe requires to later capture the payment when the subscription renews, as explained in the [Payment Flow Overview](#intermission-payment-flow-overview).
|
2. Pass the additional data that Stripe requires to later capture the payment when the subscription renews, as explained in the [Payment Flow Overview](#intermission-payment-flow-overview).
|
||||||
|
3. Change the complete cart action to use the custom API route you created in the previous step.
|
||||||
|
|
||||||
### Add Subscription Step
|
### Add Subscription Step
|
||||||
|
|
||||||
@@ -1199,6 +1232,30 @@ If you're integrating with a custom payment provider, you can instead pass the r
|
|||||||
|
|
||||||
The payment method can now be used later to capture the payment when the subscription renews.
|
The payment method can now be used later to capture the payment when the subscription renews.
|
||||||
|
|
||||||
|
### Change Complete Cart Action
|
||||||
|
|
||||||
|
Finally, you need to change the complete cart action to use the custom API route you created in the previous step.
|
||||||
|
|
||||||
|
In `src/lib/data/cart.ts`, find the `placeOrder` function and change the `await sdk.store.cart.complete` call to the following:
|
||||||
|
|
||||||
|
```ts title="src/lib/data/cart.ts" badgeLabel="Storefront" badgeColor="orange"
|
||||||
|
const cartRes = await sdk.client.fetch<{
|
||||||
|
type: "cart"
|
||||||
|
cart: HttpTypes.StoreCart
|
||||||
|
} | {
|
||||||
|
type: "order"
|
||||||
|
order: HttpTypes.StoreOrder
|
||||||
|
}>(
|
||||||
|
`/store/carts/${id}/subscribe`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You change the request to send a `POST` request to the `/store/carts/[id]/subscribe` endpoint you created earlier. You can keep the rest of the `placeOrder` function as is.
|
||||||
|
|
||||||
### Test Cart Completion and Subscription Creation
|
### Test Cart Completion and Subscription Creation
|
||||||
|
|
||||||
To test out the cart completion flow:
|
To test out the cart completion flow:
|
||||||
|
|||||||
@@ -3236,7 +3236,7 @@ const handleStep2Submit = async () => {
|
|||||||
|
|
||||||
return price
|
return price
|
||||||
}).filter((price) => price.amount > 0), // Only include prices > 0
|
}).filter((price) => price.amount > 0), // Only include prices > 0
|
||||||
}))
|
})).filter((variant) => variant.seat_count > 0) // Only create variants for row types with seats
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -3499,16 +3499,14 @@ You can edit the associated Medusa product to add images, descriptions, and othe
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 10: Validate Cart Before Checkout
|
## Step 10: Validate Ticket on Add to Cart
|
||||||
|
|
||||||
In this step, you'll add custom validation to core cart operations that ensures a seat isn't purchased more than once for the same date.
|
In this step, you'll add custom validation to the core add-to-cart operation that ensures a seat isn't purchased more than once for the same date.
|
||||||
|
|
||||||
Medusa implements cart operations in workflows. Specifically, you'll focus on the `addToCartWorkflow` and `completeCartWorkflow`. Medusa allows you to inject custom logic into workflows using [hooks](!docs!/learn/fundamentals/workflows/workflow-hooks).
|
Medusa implements cart operations in workflows. Specifically, you'll focus on the `addToCartWorkflow`. Medusa allows you to inject custom logic into workflows using [hooks](!docs!/learn/fundamentals/workflows/workflow-hooks).
|
||||||
|
|
||||||
A workflow hook is a point in a workflow where you can inject custom functionality as a step function.
|
A workflow hook is a point in a workflow where you can inject custom functionality as a step function.
|
||||||
|
|
||||||
#### Add to Cart Validation Hook
|
|
||||||
|
|
||||||
To consume the `validate` hook of the `addToCartWorkflow` that holds the add-to-cart logic, create the file `src/workflows/hooks/add-to-cart-validation.ts` with the following content:
|
To consume the `validate` hook of the `addToCartWorkflow` that holds the add-to-cart logic, create the file `src/workflows/hooks/add-to-cart-validation.ts` with the following content:
|
||||||
|
|
||||||
```ts title="src/workflows/hooks/add-to-cart-validation.ts"
|
```ts title="src/workflows/hooks/add-to-cart-validation.ts"
|
||||||
@@ -3606,91 +3604,7 @@ In the step function, you:
|
|||||||
|
|
||||||
If the hook throws an error, the add-to-cart operation will be aborted and the error message will be returned to the client.
|
If the hook throws an error, the add-to-cart operation will be aborted and the error message will be returned to the client.
|
||||||
|
|
||||||
#### Complete Cart Validation Hook
|
You can test out the hook when you [customize the storefront](./storefront/page.mdx).
|
||||||
|
|
||||||
Next, to consume the `validate` hook of the `completeCartWorkflow` that holds the checkout logic, create the file `src/workflows/hooks/complete-cart-validation.ts` with the following content:
|
|
||||||
|
|
||||||
```ts title="src/workflows/hooks/complete-cart-validation.ts"
|
|
||||||
import { completeCartWorkflow } from "@medusajs/medusa/core-flows"
|
|
||||||
import { MedusaError } from "@medusajs/framework/utils"
|
|
||||||
|
|
||||||
completeCartWorkflow.hooks.validate(
|
|
||||||
async ({ cart }, { container }) => {
|
|
||||||
const query = container.resolve("query")
|
|
||||||
|
|
||||||
const { data: items } = await query.graph({
|
|
||||||
entity: "line_item",
|
|
||||||
fields: ["id", "variant_id", "metadata", "quantity"],
|
|
||||||
filters: {
|
|
||||||
id: cart.items.map((item) => item.id).filter(Boolean) as string[],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
// Get the product variant to check if it's a ticket product variant
|
|
||||||
const { data: productVariants } = await query.graph({
|
|
||||||
entity: "product_variant",
|
|
||||||
fields: ["id", "product_id", "ticket_product_variant.purchases.*"],
|
|
||||||
filters: {
|
|
||||||
id: items.map((item) => item.variant_id).filter(Boolean) as string[],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Check for duplicate seats within the cart
|
|
||||||
const seatDateCombinations = new Set<string>()
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.quantity !== 1) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
"You can only purchase one ticket for a seat."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const productVariant = productVariants.find(
|
|
||||||
(variant) => variant.id === item.variant_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!productVariant || !item.metadata?.seat_number) {continue}
|
|
||||||
|
|
||||||
if (!item.metadata?.show_date) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Show date is required for seat ${item.metadata?.seat_number} in product ${productVariant.product_id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a unique key for seat and date combination
|
|
||||||
const seatDateKey = `${item.metadata?.seat_number}-${item.metadata?.show_date}`
|
|
||||||
|
|
||||||
// Check if this seat-date combination already exists in the cart
|
|
||||||
if (seatDateCombinations.has(seatDateKey)) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Duplicate seat ${item.metadata?.seat_number} found for show date ${item.metadata?.show_date} in cart`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the set to track this combination
|
|
||||||
seatDateCombinations.add(seatDateKey)
|
|
||||||
|
|
||||||
// Check if seat has already been purchased
|
|
||||||
const existingPurchase = productVariant.ticket_product_variant?.purchases.find(
|
|
||||||
(purchase) => purchase?.seat_number === item.metadata?.seat_number
|
|
||||||
&& purchase?.show_date === item.metadata?.show_date
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingPurchase) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Similar to the previous hook, you consume the `validate` hook of the `completeCartWorkflow` to validate that no seat is purchased more than once for the same date.
|
|
||||||
|
|
||||||
You can test out both hooks when you [customize the storefront](./storefront/page.mdx).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -3706,46 +3620,80 @@ The custom workflow that completes the cart has the following steps:
|
|||||||
workflow={{
|
workflow={{
|
||||||
name: "completeCartWithTicketsWorkflow",
|
name: "completeCartWithTicketsWorkflow",
|
||||||
steps: [
|
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",
|
type: "workflow",
|
||||||
name: "completeCartWorkflow",
|
name: "completeCartWorkflow",
|
||||||
description: "Complete the cart using Medusa's default completeCartWorkflow",
|
description: "Complete the cart using Medusa's default completeCartWorkflow",
|
||||||
depth: 1,
|
depth: 2,
|
||||||
link: "/references/medusa-workflows/completeCartWorkflow"
|
link: "/references/medusa-workflows/completeCartWorkflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the cart details",
|
description: "Retrieve the cart details",
|
||||||
depth: 2,
|
depth: 3,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "createTicketPurchasesStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Create ticket purchases for each ticket product variant in the cart",
|
description: "Retrieve existing ticket purchases to ensure idempotency",
|
||||||
depth: 3,
|
depth: 4,
|
||||||
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "when",
|
||||||
name: "createRemoteLinkStep",
|
condition: "existingLinks.length === 0",
|
||||||
description: "Create links between the order and ticket purchases",
|
steps: [
|
||||||
depth: 4,
|
{
|
||||||
link: "/references/helper-steps/createRemoteLinkStep"
|
type: "step",
|
||||||
|
name: "validateTicketOrderStep",
|
||||||
|
description: "Validate that the ticket order can be processed",
|
||||||
|
depth: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "createTicketPurchasesStep",
|
||||||
|
description: "Create ticket purchases for each ticket product variant in the cart",
|
||||||
|
depth: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "createRemoteLinkStep",
|
||||||
|
description: "Create links between the order and ticket purchases",
|
||||||
|
depth: 3,
|
||||||
|
link: "/references/helper-steps/createRemoteLinkStep"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
depth: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "step",
|
type: "step",
|
||||||
name: "useQueryGraphStep",
|
name: "useQueryGraphStep",
|
||||||
description: "Retrieve the order details",
|
description: "Retrieve the order details",
|
||||||
depth: 5,
|
depth: 6,
|
||||||
link: "/references/helper-steps/useQueryGraphStep"
|
link: "/references/helper-steps/useQueryGraphStep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "step",
|
||||||
|
name: "releaseLockStep",
|
||||||
|
description: "Release the lock on the cart",
|
||||||
|
depth: 7,
|
||||||
|
link: "/references/medusa-workflows/steps/releaseLockStep"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
hideLegend
|
hideLegend
|
||||||
/>
|
/>
|
||||||
|
|
||||||
You only need to implement the `createTicketPurchasesStep` step, as the other steps and workflows are provided by Medusa.
|
You only need to implement the `createTicketPurchasesStep` and `validateTicketOrderStep` steps, as the other steps and workflows are provided by Medusa.
|
||||||
|
|
||||||
#### createTicketPurchasesStep
|
#### createTicketPurchasesStep
|
||||||
|
|
||||||
@@ -3840,18 +3788,135 @@ In the step function, you prepare the ticket purchases to be created, create the
|
|||||||
|
|
||||||
In the compensation function, you delete the created ticket purchases if an error occurs in the workflow.
|
In the compensation function, you delete the created ticket purchases if an error occurs in the workflow.
|
||||||
|
|
||||||
|
#### validateTicketOrderStep
|
||||||
|
|
||||||
|
The `validateTicketOrderStep` validates that the tickets can be purchased based on their availability.
|
||||||
|
|
||||||
|
To create the step, create the file `src/workflows/steps/validate-ticket-order.ts` with the following content:
|
||||||
|
|
||||||
|
```ts title="src/workflows/steps/validate-ticket-order.ts"
|
||||||
|
import { MedusaError } from "@medusajs/framework/utils"
|
||||||
|
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { cancelOrderWorkflow } from "@medusajs/medusa/core-flows"
|
||||||
|
|
||||||
|
export type ValidateTicketOrderStepInput = {
|
||||||
|
items: {
|
||||||
|
id: string
|
||||||
|
variant_id: string
|
||||||
|
metadata: Record<string, unknown>
|
||||||
|
quantity: number
|
||||||
|
variant?: {
|
||||||
|
id: string
|
||||||
|
product_id: string
|
||||||
|
ticket_product_variant?: {
|
||||||
|
purchases?: {
|
||||||
|
seat_number: string
|
||||||
|
show_date: Date
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
order_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateTicketOrderStep = createStep(
|
||||||
|
"validate-ticket-order",
|
||||||
|
async ({ items, order_id }: ValidateTicketOrderStepInput, { container }) => {
|
||||||
|
// Check for duplicate seats within the cart
|
||||||
|
const seatDateCombinations = new Set<string>()
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.quantity !== 1) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
"You can only purchase one ticket for a seat."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.variant || !item.metadata?.seat_number) {continue}
|
||||||
|
|
||||||
|
if (!item.metadata?.show_date) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Show date is required for seat ${item.metadata?.seat_number} in product ${item.variant.product_id}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a unique key for seat and date combination
|
||||||
|
const seatDateKey = `${item.metadata?.seat_number}-${item.metadata?.show_date}`
|
||||||
|
|
||||||
|
// Check if this seat-date combination already exists in the cart
|
||||||
|
if (seatDateCombinations.has(seatDateKey)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Duplicate seat ${item.metadata?.seat_number} found for show date ${item.metadata?.show_date} in cart`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the set to track this combination
|
||||||
|
seatDateCombinations.add(seatDateKey)
|
||||||
|
|
||||||
|
// Check if seat has already been purchased
|
||||||
|
const existingPurchase = item.variant.ticket_product_variant?.purchases?.find(
|
||||||
|
(purchase) => purchase?.seat_number === item.metadata?.seat_number
|
||||||
|
&& purchase?.show_date === item.metadata?.show_date
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existingPurchase) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Seat ${item.metadata?.seat_number} has already been purchased for show date ${item.metadata?.show_date}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StepResponse({ validated: true }, order_id)
|
||||||
|
},
|
||||||
|
async (order_id, { container, context }) => {
|
||||||
|
if (!order_id) {return}
|
||||||
|
|
||||||
|
cancelOrderWorkflow(container).run({
|
||||||
|
input: {
|
||||||
|
order_id,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `validateTicketOrderStep` accepts the cart items and order ID as input.
|
||||||
|
|
||||||
|
In the step function, you validate that:
|
||||||
|
|
||||||
|
1. No seat is purchased more than once for the same date within the cart.
|
||||||
|
2. No seat has already been purchased for the same date.
|
||||||
|
|
||||||
|
If any validation fails, you throw an error to abort the workflow.
|
||||||
|
|
||||||
|
The step also has a compensation function that cancels the order if an error occurs later in the workflow. This is important to ensure that if ticket purchase creation fails, the order is not left in a completed state.
|
||||||
|
|
||||||
#### Custom Complete Cart Workflow
|
#### Custom Complete Cart Workflow
|
||||||
|
|
||||||
You can now create the custom workflow that completes the cart and creates ticket purchases.
|
You can now create the custom workflow that completes the cart and creates ticket purchases.
|
||||||
|
|
||||||
Create the file `src/workflows/complete-cart-with-tickets.ts` with the following content:
|
Create the file `src/workflows/complete-cart-with-tickets.ts` with the following content:
|
||||||
|
|
||||||
```ts title="src/workflows/complete-cart-with-tickets.ts"
|
```ts title="src/workflows/complete-cart-with-tickets.ts" collapsibleLines="1-14" expandButtonLabel="Show Imports"
|
||||||
import { createWorkflow, transform, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
|
||||||
import { completeCartWorkflow, createRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
|
import {
|
||||||
|
completeCartWorkflow,
|
||||||
|
createRemoteLinkStep,
|
||||||
|
acquireLockStep,
|
||||||
|
releaseLockStep,
|
||||||
|
useQueryGraphStep,
|
||||||
|
} from "@medusajs/medusa/core-flows"
|
||||||
import { createTicketPurchasesStep, CreateTicketPurchasesStepInput } from "./steps/create-ticket-purchases"
|
import { createTicketPurchasesStep, CreateTicketPurchasesStepInput } from "./steps/create-ticket-purchases"
|
||||||
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"
|
import { TICKET_BOOKING_MODULE } from "../modules/ticket-booking"
|
||||||
import { Modules } from "@medusajs/framework/utils"
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
|
import ticketPurchaseOrderLink from "../links/ticket-purchase-order"
|
||||||
|
import { validateTicketOrderStep, ValidateTicketOrderStepInput } from "./steps/validate-ticket-order"
|
||||||
|
|
||||||
export type CompleteCartWithTicketsWorkflowInput = {
|
export type CompleteCartWithTicketsWorkflowInput = {
|
||||||
cart_id: string
|
cart_id: string
|
||||||
@@ -3860,7 +3925,11 @@ export type CompleteCartWithTicketsWorkflowInput = {
|
|||||||
export const completeCartWithTicketsWorkflow = createWorkflow(
|
export const completeCartWithTicketsWorkflow = createWorkflow(
|
||||||
"complete-cart-with-tickets",
|
"complete-cart-with-tickets",
|
||||||
(input: CompleteCartWithTicketsWorkflowInput) => {
|
(input: CompleteCartWithTicketsWorkflowInput) => {
|
||||||
// Step 1: Complete the cart using Medusa's workflow
|
acquireLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
timeout: 2,
|
||||||
|
ttl: 10,
|
||||||
|
})
|
||||||
const order = completeCartWorkflow.runAsStep({
|
const order = completeCartWorkflow.runAsStep({
|
||||||
input: {
|
input: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
@@ -3876,7 +3945,9 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
|
|||||||
"items.variant.options.option.*",
|
"items.variant.options.option.*",
|
||||||
"items.variant.ticket_product_variant.*",
|
"items.variant.ticket_product_variant.*",
|
||||||
"items.variant.ticket_product_variant.ticket_product.*",
|
"items.variant.ticket_product_variant.ticket_product.*",
|
||||||
|
"items.variant.ticket_product_variant.purchases.*",
|
||||||
"items.metadata",
|
"items.metadata",
|
||||||
|
"items.quantity",
|
||||||
],
|
],
|
||||||
filters: {
|
filters: {
|
||||||
id: input.cart_id,
|
id: input.cart_id,
|
||||||
@@ -3886,31 +3957,40 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 2: Create ticket purchases for ticket products
|
const { data: existingLinks } = useQueryGraphStep({
|
||||||
const ticketPurchases = createTicketPurchasesStep({
|
entity: ticketPurchaseOrderLink.entryPoint,
|
||||||
order_id: order.id,
|
fields: ["ticket_purchase.id"],
|
||||||
cart: carts[0],
|
filters: { order_id: order.id },
|
||||||
} as unknown as CreateTicketPurchasesStepInput)
|
}).config({ name: "retrieve-existing-links" })
|
||||||
|
|
||||||
// Step 3: Link ticket purchases to the order
|
when({ existingLinks }, (data) => data.existingLinks.length === 0)
|
||||||
const linkData = transform({
|
.then(() => {
|
||||||
order,
|
validateTicketOrderStep({
|
||||||
ticketPurchases,
|
items: carts[0].items,
|
||||||
}, (data) => {
|
order_id: order.id,
|
||||||
return data.ticketPurchases.map((purchase) => ({
|
} as unknown as ValidateTicketOrderStepInput)
|
||||||
[TICKET_BOOKING_MODULE]: {
|
const ticketPurchases = createTicketPurchasesStep({
|
||||||
ticket_purchase_id: purchase.id,
|
order_id: order.id,
|
||||||
},
|
cart: carts[0],
|
||||||
[Modules.ORDER]: {
|
} as unknown as CreateTicketPurchasesStepInput)
|
||||||
order_id: data.order.id,
|
|
||||||
},
|
const linkData = transform({
|
||||||
}))
|
order,
|
||||||
|
ticketPurchases,
|
||||||
|
}, (data) => {
|
||||||
|
return data.ticketPurchases.map((purchase) => ({
|
||||||
|
[TICKET_BOOKING_MODULE]: {
|
||||||
|
ticket_purchase_id: purchase.id,
|
||||||
|
},
|
||||||
|
[Modules.ORDER]: {
|
||||||
|
order_id: data.order.id,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
createRemoteLinkStep(linkData)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 4: Create remote links
|
|
||||||
createRemoteLinkStep(linkData)
|
|
||||||
|
|
||||||
// Step 5: Fetch order details
|
|
||||||
const { data: refetchedOrder } = useQueryGraphStep({
|
const { data: refetchedOrder } = useQueryGraphStep({
|
||||||
entity: "order",
|
entity: "order",
|
||||||
fields: [
|
fields: [
|
||||||
@@ -3934,6 +4014,10 @@ export const completeCartWithTicketsWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
}).config({ name: "refetch-order" })
|
}).config({ name: "refetch-order" })
|
||||||
|
|
||||||
|
releaseLockStep({
|
||||||
|
key: input.cart_id,
|
||||||
|
})
|
||||||
|
|
||||||
return new WorkflowResponse({
|
return new WorkflowResponse({
|
||||||
order: refetchedOrder[0],
|
order: refetchedOrder[0],
|
||||||
})
|
})
|
||||||
@@ -3945,11 +4029,17 @@ The `completeCartWithTicketsWorkflow` accepts the cart ID as input.
|
|||||||
|
|
||||||
In the workflow function, you:
|
In the workflow function, you:
|
||||||
|
|
||||||
1. Complete the cart using Medusa's `completeCartWorkflow`.
|
1. Acquire a lock on the cart to prevent concurrent modifications using the `acquireLockStep`.
|
||||||
2. Retrieve the cart details using the `useQueryGraphStep`.
|
2. Complete the cart using Medusa's `completeCartWorkflow`.
|
||||||
3. Create ticket purchases for each ticket product variant in the cart using the `createTicketPurchasesStep`.
|
3. Retrieve the cart details using the `useQueryGraphStep`.
|
||||||
4. Create links between the order and the created ticket purchases using the `createRemoteLinkStep`.
|
4. Retrieve existing ticket purchases linked to the order to ensure idempotency using the `useQueryGraphStep`.
|
||||||
5. Retrieve the order details using the `useQueryGraphStep`.
|
- This is important because if the workflow is retried, you don't want to create duplicate ticket purchases.
|
||||||
|
5. Use `when` to check that there are no existing links between the order and ticket purchases. If so, you:
|
||||||
|
1. Validate that the ticket order can be processed using the `validateTicketOrderStep`.
|
||||||
|
2. Create ticket purchases for each ticket product variant in the cart using the `createTicketPurchasesStep`.
|
||||||
|
3. Create links between the order and the created ticket purchases using the `createRemoteLinkStep`.
|
||||||
|
6. Retrieve the order details using the `useQueryGraphStep`.
|
||||||
|
7. Release the lock on the cart using the `releaseLockStep`.
|
||||||
|
|
||||||
Finally, you return the order details.
|
Finally, you return the order details.
|
||||||
|
|
||||||
|
|||||||
@@ -110,17 +110,17 @@ export const generatedEditDates = {
|
|||||||
"app/nextjs-starter/page.mdx": "2025-02-26T11:37:47.137Z",
|
"app/nextjs-starter/page.mdx": "2025-02-26T11:37:47.137Z",
|
||||||
"app/recipes/b2b/page.mdx": "2025-05-20T07:51:40.718Z",
|
"app/recipes/b2b/page.mdx": "2025-05-20T07:51:40.718Z",
|
||||||
"app/recipes/commerce-automation/page.mdx": "2025-05-20T07:51:40.719Z",
|
"app/recipes/commerce-automation/page.mdx": "2025-05-20T07:51:40.719Z",
|
||||||
"app/recipes/digital-products/examples/standard/page.mdx": "2025-10-21T10:31:35.296Z",
|
"app/recipes/digital-products/examples/standard/page.mdx": "2025-11-28T09:43:09.959Z",
|
||||||
"app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z",
|
"app/recipes/digital-products/page.mdx": "2025-05-20T07:51:40.719Z",
|
||||||
"app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z",
|
"app/recipes/ecommerce/page.mdx": "2025-06-24T08:50:10.116Z",
|
||||||
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-10-21T10:47:23.415Z",
|
"app/recipes/marketplace/examples/vendors/page.mdx": "2025-11-28T10:09:41.993Z",
|
||||||
"app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z",
|
"app/recipes/marketplace/page.mdx": "2025-05-20T07:51:40.721Z",
|
||||||
"app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z",
|
"app/recipes/multi-region-store/page.mdx": "2025-05-20T07:51:40.721Z",
|
||||||
"app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z",
|
"app/recipes/omnichannel/page.mdx": "2025-05-20T07:51:40.722Z",
|
||||||
"app/recipes/oms/page.mdx": "2025-05-20T07:51:40.722Z",
|
"app/recipes/oms/page.mdx": "2025-05-20T07:51:40.722Z",
|
||||||
"app/recipes/personalized-products/page.mdx": "2025-07-22T06:52:29.419Z",
|
"app/recipes/personalized-products/page.mdx": "2025-07-22T06:52:29.419Z",
|
||||||
"app/recipes/pos/page.mdx": "2025-05-20T07:51:40.723Z",
|
"app/recipes/pos/page.mdx": "2025-05-20T07:51:40.723Z",
|
||||||
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-10-21T12:36:28.745Z",
|
"app/recipes/subscriptions/examples/standard/page.mdx": "2025-11-28T12:02:36.170Z",
|
||||||
"app/recipes/subscriptions/page.mdx": "2025-05-20T07:51:40.723Z",
|
"app/recipes/subscriptions/page.mdx": "2025-05-20T07:51:40.723Z",
|
||||||
"app/recipes/page.mdx": "2025-05-20T07:51:40.722Z",
|
"app/recipes/page.mdx": "2025-05-20T07:51:40.722Z",
|
||||||
"app/service-factory-reference/methods/create/page.mdx": "2025-09-01T15:54:53.385Z",
|
"app/service-factory-reference/methods/create/page.mdx": "2025-09-01T15:54:53.385Z",
|
||||||
@@ -5864,7 +5864,7 @@ export const generatedEditDates = {
|
|||||||
"app/commerce-modules/payment/account-holder/page.mdx": "2025-04-07T07:31:20.235Z",
|
"app/commerce-modules/payment/account-holder/page.mdx": "2025-04-07T07:31:20.235Z",
|
||||||
"app/troubleshooting/test-errors/page.mdx": "2025-10-28T16:02:39.347Z",
|
"app/troubleshooting/test-errors/page.mdx": "2025-10-28T16:02:39.347Z",
|
||||||
"app/commerce-modules/product/variant-inventory/page.mdx": "2025-04-25T13:25:02.408Z",
|
"app/commerce-modules/product/variant-inventory/page.mdx": "2025-04-25T13:25:02.408Z",
|
||||||
"app/examples/guides/custom-item-price/page.mdx": "2025-06-26T11:53:06.748Z",
|
"app/examples/guides/custom-item-price/page.mdx": "2025-11-28T08:30:59.896Z",
|
||||||
"references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateShippingStep/page.mdx": "2025-04-11T09:04:35.729Z",
|
"references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateShippingStep/page.mdx": "2025-04-11T09:04:35.729Z",
|
||||||
"references/core_flows/Cart/Steps_Cart/variables/core_flows.Cart.Steps_Cart.validateShippingStepId/page.mdx": "2025-02-11T11:36:39.228Z",
|
"references/core_flows/Cart/Steps_Cart/variables/core_flows.Cart.Steps_Cart.validateShippingStepId/page.mdx": "2025-02-11T11:36:39.228Z",
|
||||||
"references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createPaymentAccountHolderStep/page.mdx": "2025-02-24T10:48:31.714Z",
|
"references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createPaymentAccountHolderStep/page.mdx": "2025-02-24T10:48:31.714Z",
|
||||||
@@ -6042,7 +6042,7 @@ export const generatedEditDates = {
|
|||||||
"app/integrations/guides/algolia/page.mdx": "2025-11-27T08:21:37.971Z",
|
"app/integrations/guides/algolia/page.mdx": "2025-11-27T08:21:37.971Z",
|
||||||
"app/integrations/guides/magento/page.mdx": "2025-10-09T11:30:09.533Z",
|
"app/integrations/guides/magento/page.mdx": "2025-10-09T11:30:09.533Z",
|
||||||
"app/js-sdk/auth/overview/page.mdx": "2025-03-28T08:05:32.622Z",
|
"app/js-sdk/auth/overview/page.mdx": "2025-03-28T08:05:32.622Z",
|
||||||
"app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-10-09T11:27:14.961Z",
|
"app/how-to-tutorials/tutorials/loyalty-points/page.mdx": "2025-11-28T08:48:32.647Z",
|
||||||
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.plugin/page.mdx": "2025-04-11T09:04:55.084Z",
|
"references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.plugin/page.mdx": "2025-04-11T09:04:55.084Z",
|
||||||
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/page.mdx": "2025-05-20T07:51:40.936Z",
|
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/page.mdx": "2025-05-20T07:51:40.936Z",
|
||||||
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/page.mdx": "2025-04-11T09:04:54.015Z",
|
"references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/page.mdx": "2025-04-11T09:04:54.015Z",
|
||||||
@@ -6473,7 +6473,7 @@ export const generatedEditDates = {
|
|||||||
"references/types/interfaces/types.WebhookActionResult/page.mdx": "2025-05-20T07:51:41.086Z",
|
"references/types/interfaces/types.WebhookActionResult/page.mdx": "2025-05-20T07:51:41.086Z",
|
||||||
"references/types/interfaces/types.WebhookActionData/page.mdx": "2025-05-20T07:51:41.086Z",
|
"references/types/interfaces/types.WebhookActionData/page.mdx": "2025-05-20T07:51:41.086Z",
|
||||||
"app/commerce-modules/tax/tax-provider/page.mdx": "2025-10-22T07:14:23.461Z",
|
"app/commerce-modules/tax/tax-provider/page.mdx": "2025-10-22T07:14:23.461Z",
|
||||||
"app/recipes/bundled-products/examples/standard/page.mdx": "2025-06-26T11:52:18.819Z",
|
"app/recipes/bundled-products/examples/standard/page.mdx": "2025-11-28T08:10:29.485Z",
|
||||||
"app/recipes/bundled-products/page.mdx": "2025-05-20T07:51:40.718Z",
|
"app/recipes/bundled-products/page.mdx": "2025-05-20T07:51:40.718Z",
|
||||||
"app/infrastructure-modules/analytics/local/page.mdx": "2025-08-21T05:30:26.867Z",
|
"app/infrastructure-modules/analytics/local/page.mdx": "2025-08-21T05:30:26.867Z",
|
||||||
"app/infrastructure-modules/analytics/page.mdx": "2025-08-21T05:31:17.953Z",
|
"app/infrastructure-modules/analytics/page.mdx": "2025-08-21T05:31:17.953Z",
|
||||||
@@ -6547,15 +6547,15 @@ export const generatedEditDates = {
|
|||||||
"app/how-to-tutorials/tutorials/re-order/page.mdx": "2025-06-26T12:38:24.308Z",
|
"app/how-to-tutorials/tutorials/re-order/page.mdx": "2025-06-26T12:38:24.308Z",
|
||||||
"app/commerce-modules/promotion/promotion-taxes/page.mdx": "2025-06-27T15:44:46.638Z",
|
"app/commerce-modules/promotion/promotion-taxes/page.mdx": "2025-06-27T15:44:46.638Z",
|
||||||
"app/troubleshooting/payment/page.mdx": "2025-07-16T10:20:24.799Z",
|
"app/troubleshooting/payment/page.mdx": "2025-07-16T10:20:24.799Z",
|
||||||
"app/recipes/personalized-products/example/page.mdx": "2025-10-31T16:39:17.453Z",
|
"app/recipes/personalized-products/example/page.mdx": "2025-11-28T10:13:26.457Z",
|
||||||
"app/how-to-tutorials/tutorials/preorder/page.mdx": "2025-10-09T11:33:06.479Z",
|
"app/how-to-tutorials/tutorials/preorder/page.mdx": "2025-11-28T10:55:29.378Z",
|
||||||
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.archive/page.mdx": "2025-09-12T14:10:49.018Z",
|
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.archive/page.mdx": "2025-09-12T14:10:49.018Z",
|
||||||
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.complete/page.mdx": "2025-09-12T14:10:49.024Z",
|
"references/js_sdk/admin/Order/methods/js_sdk.admin.Order.complete/page.mdx": "2025-09-12T14:10:49.024Z",
|
||||||
"app/commerce-modules/cart/cart-totals/page.mdx": "2025-07-31T15:18:13.978Z",
|
"app/commerce-modules/cart/cart-totals/page.mdx": "2025-07-31T15:18:13.978Z",
|
||||||
"app/commerce-modules/order/order-totals/page.mdx": "2025-07-31T15:12:10.633Z",
|
"app/commerce-modules/order/order-totals/page.mdx": "2025-07-31T15:12:10.633Z",
|
||||||
"app/commerce-modules/user/invite-user-subscriber/page.mdx": "2025-08-01T12:01:54.551Z",
|
"app/commerce-modules/user/invite-user-subscriber/page.mdx": "2025-08-01T12:01:54.551Z",
|
||||||
"app/how-to-tutorials/tutorials/invoice-generator/page.mdx": "2025-10-09T11:26:43.515Z",
|
"app/how-to-tutorials/tutorials/invoice-generator/page.mdx": "2025-10-09T11:26:43.515Z",
|
||||||
"app/how-to-tutorials/tutorials/product-builder/page.mdx": "2025-10-09T11:28:24.756Z",
|
"app/how-to-tutorials/tutorials/product-builder/page.mdx": "2025-11-28T11:00:31.676Z",
|
||||||
"app/integrations/guides/payload/page.mdx": "2025-10-22T15:05:39.648Z",
|
"app/integrations/guides/payload/page.mdx": "2025-10-22T15:05:39.648Z",
|
||||||
"references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken/page.mdx": "2025-08-14T12:59:55.678Z",
|
"references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken/page.mdx": "2025-08-14T12:59:55.678Z",
|
||||||
"app/commerce-modules/order/draft-orders/page.mdx": "2025-08-26T09:21:49.780Z",
|
"app/commerce-modules/order/draft-orders/page.mdx": "2025-08-26T09:21:49.780Z",
|
||||||
@@ -6565,7 +6565,7 @@ export const generatedEditDates = {
|
|||||||
"app/storefront-development/cart/manage-promotions/page.mdx": "2025-09-11T14:11:40.904Z",
|
"app/storefront-development/cart/manage-promotions/page.mdx": "2025-09-11T14:11:40.904Z",
|
||||||
"app/recipes/ticket-booking/examples/page.mdx": "2025-09-10T14:11:55.063Z",
|
"app/recipes/ticket-booking/examples/page.mdx": "2025-09-10T14:11:55.063Z",
|
||||||
"app/recipes/ticket-booking/examples/storefront/page.mdx": "2025-09-10T14:14:44.005Z",
|
"app/recipes/ticket-booking/examples/storefront/page.mdx": "2025-09-10T14:14:44.005Z",
|
||||||
"app/recipes/ticket-booking/example/page.mdx": "2025-09-10T15:13:15.604Z",
|
"app/recipes/ticket-booking/example/page.mdx": "2025-11-28T13:02:15.777Z",
|
||||||
"app/recipes/ticket-booking/example/storefront/page.mdx": "2025-09-10T15:26:04.084Z",
|
"app/recipes/ticket-booking/example/storefront/page.mdx": "2025-09-10T15:26:04.084Z",
|
||||||
"app/recipes/ticket-booking/page.mdx": "2025-09-11T06:53:21.071Z",
|
"app/recipes/ticket-booking/page.mdx": "2025-09-11T06:53:21.071Z",
|
||||||
"references/js_sdk/admin/Views/methods/js_sdk.admin.Views.columns/page.mdx": "2025-10-21T08:10:57.659Z",
|
"references/js_sdk/admin/Views/methods/js_sdk.admin.Views.columns/page.mdx": "2025-10-21T08:10:57.659Z",
|
||||||
@@ -6612,7 +6612,7 @@ export const generatedEditDates = {
|
|||||||
"app/data-model-repository-reference/methods/upsert/page.mdx": "2025-10-28T16:02:28.730Z",
|
"app/data-model-repository-reference/methods/upsert/page.mdx": "2025-10-28T16:02:28.730Z",
|
||||||
"app/data-model-repository-reference/methods/update/page.mdx": "2025-10-28T16:02:27.582Z",
|
"app/data-model-repository-reference/methods/update/page.mdx": "2025-10-28T16:02:27.582Z",
|
||||||
"app/data-model-repository-reference/methods/upsertWithReplace/page.mdx": "2025-10-28T16:02:30.479Z",
|
"app/data-model-repository-reference/methods/upsertWithReplace/page.mdx": "2025-10-28T16:02:30.479Z",
|
||||||
"app/how-to-tutorials/tutorials/agentic-commerce/page.mdx": "2025-10-09T11:25:48.831Z",
|
"app/how-to-tutorials/tutorials/agentic-commerce/page.mdx": "2025-11-28T07:37:38.151Z",
|
||||||
"app/storefront-development/production-optimizations/page.mdx": "2025-10-03T13:28:37.909Z",
|
"app/storefront-development/production-optimizations/page.mdx": "2025-10-03T13:28:37.909Z",
|
||||||
"app/how-to-tutorials/tutorials/category-images/page.mdx": "2025-11-07T08:55:59.228Z",
|
"app/how-to-tutorials/tutorials/category-images/page.mdx": "2025-11-07T08:55:59.228Z",
|
||||||
"app/infrastructure-modules/caching/page.mdx": "2025-11-13T14:18:03.173Z",
|
"app/infrastructure-modules/caching/page.mdx": "2025-11-13T14:18:03.173Z",
|
||||||
@@ -6682,7 +6682,7 @@ export const generatedEditDates = {
|
|||||||
"references/utils/PromotionUtils/enums/utils.PromotionUtils.ApplicationMethodAllocation/page.mdx": "2025-10-21T08:10:52.665Z",
|
"references/utils/PromotionUtils/enums/utils.PromotionUtils.ApplicationMethodAllocation/page.mdx": "2025-10-21T08:10:52.665Z",
|
||||||
"references/utils/PromotionUtils/enums/utils.PromotionUtils.CampaignBudgetType/page.mdx": "2025-10-21T08:10:52.672Z",
|
"references/utils/PromotionUtils/enums/utils.PromotionUtils.CampaignBudgetType/page.mdx": "2025-10-21T08:10:52.672Z",
|
||||||
"app/integrations/guides/avalara/page.mdx": "2025-10-22T09:56:11.929Z",
|
"app/integrations/guides/avalara/page.mdx": "2025-10-22T09:56:11.929Z",
|
||||||
"app/how-to-tutorials/tutorials/product-rentals/page.mdx": "2025-10-28T16:09:26.244Z",
|
"app/how-to-tutorials/tutorials/product-rentals/page.mdx": "2025-11-28T11:39:49.672Z",
|
||||||
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchImageVariants/page.mdx": "2025-10-31T09:41:42.515Z",
|
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchImageVariants/page.mdx": "2025-10-31T09:41:42.515Z",
|
||||||
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantImages/page.mdx": "2025-10-31T09:41:42.517Z",
|
"references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantImages/page.mdx": "2025-10-31T09:41:42.517Z",
|
||||||
"references/product/IProductModuleService/methods/product.IProductModuleService.addImageToVariant/page.mdx": "2025-10-31T09:41:35.918Z",
|
"references/product/IProductModuleService/methods/product.IProductModuleService.addImageToVariant/page.mdx": "2025-10-31T09:41:35.918Z",
|
||||||
@@ -6718,6 +6718,6 @@ export const generatedEditDates = {
|
|||||||
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchImageVariantsWorkflow/page.mdx": "2025-11-05T12:22:20.639Z",
|
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchImageVariantsWorkflow/page.mdx": "2025-11-05T12:22:20.639Z",
|
||||||
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchVariantImagesWorkflow/page.mdx": "2025-11-05T12:22:20.671Z",
|
"references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchVariantImagesWorkflow/page.mdx": "2025-11-05T12:22:20.671Z",
|
||||||
"app/storefront-development/guides/react-native-expo/page.mdx": "2025-11-06T07:18:45.347Z",
|
"app/storefront-development/guides/react-native-expo/page.mdx": "2025-11-06T07:18:45.347Z",
|
||||||
"app/how-to-tutorials/tutorials/customer-tiers/page.mdx": "2025-11-25T08:24:24.566Z",
|
"app/how-to-tutorials/tutorials/customer-tiers/page.mdx": "2025-11-28T08:34:06.912Z",
|
||||||
"app/infrastructure-modules/caching/guides/clear-cache/page.mdx": "2025-11-26T13:19:26.629Z"
|
"app/infrastructure-modules/caching/guides/clear-cache/page.mdx": "2025-11-26T13:19:26.629Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user