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

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