From f81eb51b67bf97bce12ac4a332408710a1eaedd8 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Thu, 8 May 2025 12:48:44 +0300 Subject: [PATCH] docs: add copy subscriber button (#12405) * docs: add copy subscriber button * re-generate * fixes + update copy button --- .../DescriptionSection/Events/index.tsx | 118 +- www/apps/api-reference/package.json | 4 +- www/apps/book/package.json | 2 +- www/apps/book/public/llms-full.txt | 37513 ++++++++-------- www/apps/book/scripts/prepare.mjs | 12 +- .../pricing/price-calculation/page.mdx | 2 +- .../pricing/price-rules/page.mdx | 4 +- .../examples/standard/page.mdx | 214 +- .../components/EventHeader/index.tsx | 78 + .../components/MDXComponents/index.tsx | 4 + www/apps/resources/generated/edit-dates.mjs | 668 +- www/apps/resources/package.json | 2 +- www/apps/ui/package.json | 6 +- www/apps/user-guide/package.json | 2 +- www/packages/docs-ui/package.json | 4 +- .../src/components/CopyButton/index.tsx | 9 +- .../CopyGeneratedSnippetButton/index.tsx | 31 + .../src/components/Heading/H1/index.tsx | 2 +- .../src/components/Heading/H2/index.tsx | 2 +- .../src/components/Heading/H3/index.tsx | 2 +- .../src/components/Heading/H4/index.tsx | 2 +- .../src/components/LlmDropdown/index.tsx | 75 +- .../src/components/Menu/Dropdown/index.tsx | 76 + .../docs-ui/src/components/Menu/index.tsx | 2 + www/packages/docs-ui/src/components/index.ts | 1 + www/packages/docs-ui/src/hooks/index.ts | 1 + .../src/hooks/use-generate-snippet/index.tsx | 22 + .../snippet-generators/subscriber.ts | 65 + .../docs-ui/src/utils/event-parser.ts | 54 + www/packages/docs-ui/src/utils/index.ts | 1 + www/packages/docs-utils/src/estree-to-js.ts | 22 +- www/packages/docs-utils/src/get-clean-md.ts | 2 + www/packages/docs-utils/src/utils/parsers.ts | 52 +- www/packages/types/package.json | 2 +- www/packages/types/src/remark-rehype.ts | 13 + .../src/resources/helpers/events-listing.ts | 79 +- .../src/resources/helpers/workflow-events.ts | 23 +- www/yarn.lock | 61 +- 38 files changed, 20525 insertions(+), 18707 deletions(-) create mode 100644 www/apps/resources/components/EventHeader/index.tsx create mode 100644 www/packages/docs-ui/src/components/CopyGeneratedSnippetButton/index.tsx create mode 100644 www/packages/docs-ui/src/components/Menu/Dropdown/index.tsx create mode 100644 www/packages/docs-ui/src/hooks/use-generate-snippet/index.tsx create mode 100644 www/packages/docs-ui/src/hooks/use-generate-snippet/snippet-generators/subscriber.ts create mode 100644 www/packages/docs-ui/src/utils/event-parser.ts diff --git a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx index ba8b2afafd..edf9c9ece4 100644 --- a/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx +++ b/www/apps/api-reference/components/Tags/Operation/DescriptionSection/Events/index.tsx @@ -3,18 +3,23 @@ import { Badge, DetailsSummary, + DropdownMenu, Link, MarkdownContent, + parseEventPayload, Tabs, TabsContent, TabsContentWrapper, TabsList, TabsTrigger, Tooltip, + useCopy, + useGenerateSnippet, } from "docs-ui" import { useMemo } from "react" import type { OpenAPI } from "types" import TagOperationParameters from "../../Parameters" +import { Brackets, CheckCircle, SquareTwoStack, Tag } from "@medusajs/icons" export type TagsOperationDescriptionSectionEventsProps = { events: OpenAPI.OasEvents[] @@ -68,62 +73,73 @@ const TagsOperationDescriptionSectionEvent = ({ }: { event: OpenAPI.OasEvents }) => { - const parsedPayload: OpenAPI.SchemaObject = useMemo(() => { - const payloadParams = event.payload.matchAll( - /([\w_]+),? \/\/ (\(\w*\) )*(.*)/g - ) - const payload = Array.from(payloadParams).map((match) => { - return { - name: match[1], - type: match[2]?.replace(/\(|\)/g, "") || "string", - description: match[3], - } - }) - return { - type: "object", - required: ["payload"], - properties: { - payload: { - type: "object", - description: "The payload emitted with the event", - required: [...payload.map((param) => param.name)], - properties: payload.reduce( - (acc, curr) => { - acc[curr.name] = { - type: curr.type as OpenAPI.OpenAPIV3.NonArraySchemaObjectType, - description: curr.description, - properties: {}, - } - return acc - }, - {} as Record - ), - }, - }, - } + const { + parsed_payload: parsedPayload, + payload_for_snippet: payloadForSnippet, + } = useMemo(() => { + return parseEventPayload(event.payload) }, [event.payload]) + const { snippet } = useGenerateSnippet({ + type: "subscriber", + options: { + event: event.name, + payload: payloadForSnippet, + }, + }) + const { handleCopy: handleEventNameCopy, isCopied: eventNameCopied } = + useCopy(event.name) + const { handleCopy: handleSnippetCopy, isCopied: snippetCopied } = + useCopy(snippet) return ( -
- - {`\`${event.name}\`: ${event.description}`} - - {event.deprecated && - (event.deprecated_message ? ( - +
+
+ + {event.description} + + {event.deprecated && + (event.deprecated_message ? ( + + Deprecated + + ) : ( Deprecated + ))} + {event.version && ( + + v{event.version} - ) : ( - Deprecated - ))} - {event.version && ( - - v{event.version} - - )} + )} +
+ + {eventNameCopied || snippetCopied ? ( + + ) : ( + + )} + + } + menuItems={[ + { + type: "action", + title: "Copy event name", + action: () => handleEventNameCopy(), + icon: , + }, + { + type: "action", + title: "Copy subscriber for event", + action: () => handleSnippetCopy(), + icon: , + }, + ]} + menuClassName="z-10" + />
{ + res.json({ + message: "[GET] Hello world!", + }) +} +``` + +### Test API Route + +To test the API route above, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, send a `GET` request to the `/hello-world` API Route: + +```bash +curl http://localhost:9000/hello-world +``` + +*** + +## When to Use API Routes + +You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. + + +# Custom CLI Scripts + +In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. + +## What is a Custom CLI Script? + +A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. + +*** + +## How to Create a Custom CLI Script? + +To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. + +For example, create the file `src/scripts/my-script.ts` with the following content: + +```ts title="src/scripts/my-script.ts" +import { + ExecArgs, + IProductModuleService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" + +export default async function myScript({ container }: ExecArgs) { + const productModuleService: IProductModuleService = container.resolve( + Modules.PRODUCT + ) + + const [, count] = await productModuleService + .listAndCountProducts() + + console.log(`You have ${count} product(s)`) +} +``` + +The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. + +*** + +## How to Run Custom CLI Script? + +To run the custom CLI script, run the Medusa CLI's `exec` command: + +```bash +npx medusa exec ./src/scripts/my-script.ts +``` + +*** + +## Custom CLI Script Arguments + +Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. + +For example: + +```ts +import { ExecArgs } from "@medusajs/framework/types" + +export default async function myScript({ args }: ExecArgs) { + console.log(`The arguments you passed: ${args}`) +} +``` + +Then, pass the arguments in the `exec` command after the file path: + +```bash +npx medusa exec ./src/scripts/my-script.ts arg1 arg2 +``` + + +# Data Models + +In this chapter, you'll learn what a data model is and how to create a data model. + +## What is a Data Model? + +A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. + +*** + +## How to Create a Data Model + +In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. + +So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: + +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) + +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) + +export default Post +``` + +You define the data model using the `define` method of the DML. It accepts two parameters: + +1. The first one is the name of the data model's table in the database. Use snake-case names. +2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. + - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. + +The code snippet above defines a `Post` data model with `id` and `title` properties. + +*** + +## Generate Migrations + +After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: + +If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. + +```bash +npx medusa db:generate blog +``` + +The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: + +```ts +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241121103722 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"post\" cascade;") + } + +} +``` + +In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. + +### Run Migrations + +To reflect the changes in the generated migration file on the database, run the `db:migrate` command: + +If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. + +```bash +npx medusa db:migrate +``` + +This creates the `post` table in the database. + +### Migrations on Data Model Changes + +Whenever you make a change to a data model, you must generate and run the migrations. + +For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. + +*** + +## Manage Data Models + +Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. + +For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. + +Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. + + +# Environment Variables + +In this chapter, you'll learn how environment variables are loaded in Medusa. + +## System Environment Variables + +The Medusa application loads and uses system environment variables. + +For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. + +In production, you should always use system environment variables that you set through your hosting provider. + +*** + +## Environment Variables in .env Files + +During development, it's easier to set environment variables in a `.env` file in your repository. + +Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: + +As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. + +|\`.env\`| +|---|---| +|\`NODE\_ENV\`|\`.env\`| +|\`NODE\_ENV\`|\`.env.production\`| +|\`NODE\_ENV\`|\`.env.staging\`| +|\`NODE\_ENV\`|\`.env.test\`| + +### Set Environment in `loadEnv` + +In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. + +This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. + +To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. + +*** + +## Environment Variables for Admin Customizations + +Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +*** + +## Predefined Medusa Environment Variables + +The Medusa application uses the following predefined environment variables that you can set: + +You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). + +|Environment Variable|Description|Default| +|---|---|---|---|---| +| +| +| +| +||The URL to connect to the PostgreSQL database. Only used if || +||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || +||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || +||URLs of clients that can access the Medusa backend's authentication routes. Only used if || +||A random string used to create authentication tokens in the http layer. Only used if || +||A random string used to create cookie tokens in the http layer. Only used if || +||The URL to the Medusa backend. Only used if || +| +| +| +| +| +| +| +| +||The allowed levels to log. Learn more in || +||The file to save logs in. By default, logs aren't saved in any file. Learn more in || +||Whether to disable analytics data collection. Learn more in || + + +# Events and Subscribers + +In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. + +## Handle Core Commerce Flows with Events + +When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system. + +Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase. + +You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. + +![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg) + +Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it. + +If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead. + +### List of Emitted Events + +Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md). + +*** + +## How to Create a Subscriber? + +You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to. + +For example, create the file `src/subscribers/order-placed.ts` with the following content: + +![Example of subscriber file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866244/Medusa%20Book/subscriber-dir-overview_pusyeu.jpg) + +```ts title="src/subscribers/product-created.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const logger = container.resolve("logger") + + logger.info("Sending confirmation email...") + + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: `order.placed`, +} +``` + +This subscriber file exports: + +- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. +- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. + +The subscriber function receives an object as a parameter that has the following properties: + +- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case. +- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources. + +In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber. + +*** + +## Test the Subscriber + +To test the subscriber, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, try placing an order either using Medusa's API routes or the [Next.js Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal: + +```bash +info: Processing order.placed which has 1 subscribers +Sending confirmation email... +``` + +The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. + +*** + +## Event Module + +The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system. + +Medusa provides two Event Modules out of the box: + +- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it. +- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system. + +Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md). + + +# Framework Overview + +In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application. + +## What is the Medusa Framework? + +All commerce application require some degree of customization. So, it's important to choose a platform that facilitates building those customizations. + +When you build customizations with other ecommerce platforms, they require you to pull data through HTTP APIs, run custom logic that span across systems in a separate application, and manually ensure data consistency across systems. This adds significant overhead and slows down development as you spend time managing complex distributed systems. + +The Medusa Framework eliminates this overhead by providing powerful low-level APIs and tools that let you build any type of customization directly within your Medusa project. You can build custom features, orchestrate operations and query data seamlessy across systems, extend core functionality, and automate tasks in your Medusa application. + +With the Medusa Framework, you can focus your efforts on building meaningful business customizations and continuously delivering new features. + +Using the Medusa Framework, you can build customizations like: + +- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md) +- [Deep integration with an ERP system](https://docs.medusajs.com/resources/recipes/erp/index.html.md) +- [CMS integration with seamless content retrieval](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) +- [Custom item pricing in the cart](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md) +- [Automated restock notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md) +- [Re-usable payment provider integrations](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) + +### Framework Concepts and Tools + +- [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) +- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) +- [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) +- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) + +*** + +## Build Custom Features + +The Medusa Framework allows you to build custom features tailored to your business needs. + +To create a custom feature, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that contains your feature's data models and the logic to manage them. A module is integrated into your Medusa application without side effects. + +### Data Model + +```ts highlights={modelHighlights} +import { model } from "@medusajs/framework/utils" + +export const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) +``` + +### Service + +```ts highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Post } from "./post" + +export class BlogModuleService extends MedusaService({ + Post, +}){ + // CRUD methods generated by MedusaService +} +``` + +### Module Definition + +```ts +import { Module } from "@medusajs/framework/utils" +import { BlogModuleService } from "./service" + +export const BLOG_MODULE = "blog" + +export default Module(BLOG_MODULE, { + service: BlogModuleService, +}) +``` + +Then, you can build commerce features and flows in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use your module. By using workflows, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) and [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). + +### Step + +```ts highlights={stepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { BlogModuleService, BLOG_MODULE } from "../../modules/blog" + +type Input = { + title: string +} + +const createPostStep = createStep( + "create-post", + async (input: Input, { container }) => { + const blogModuleService: BlogModuleService = container.resolve( + BLOG_MODULE + ) + + const post = await blogModuleService.createPosts(input.title) + + return new StepResponse(post, post.id) + }, + async (postId, { container }) => { + if (!postId) { + return + } + + const blogModuleService: BlogModuleService = container.resolve( + BLOG_MODULE + ) + + await blogModuleService.deletePosts(postId) + } +) +``` + +### Workflow + +```ts +import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" +import { createPostStep } from "./steps" + +type Input = { + title: string +} + +export const createPostWorkflow = createWorkflow( + "create-post", + (input: Input) => { + const post = createPostStep(input) + + return new WorkflowResponse(post) + } +) +``` + +Finally, you can expose your custom feature with [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are built on top of your module and workflows. + +```ts title="API Route Example" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPostWorkflow } from "../../../workflows/create-post" + +type PostRequestBody = { + title: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createPostWorkflow(req.scope) + .run({ + input: result.validatedBody, + }) + + return res.json(result) +} +``` + +### Examples + +The following tutorials are step-by-step guides that show you how to build custom features using the Medusa Framework. + +- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md): Build a product reviews feature in your Medusa application. +- [Wishlist](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist feature in your Medusa application. + +### Start Learning + +To learn more about the different concepts useful for building custom features, check out the following chapters: + +- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) + +*** + +## Extend Existing Features + +The Medusa Framework is flexible and extensible, allowing you to extend and build on top of existing models and features. + +To associate new properties and relations with an existing model, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with data models that define these additions. Then, you can define a [module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that associates two data models from separate modules. + +### Module Link + +```ts highlights={defineLinkHighlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` + +### Data Model + +```ts +import { model } from "@medusajs/framework/utils" + +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), +}) +``` + +### Service + +```ts +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { + +} + +export default BrandModuleService +``` + +### Module Definition + +```ts +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" + +export const BRAND_MODULE = "brand" + +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` + +Then, you can [hook into existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to perform custom actions as part of existing features and flows. For example, you can create a brand when a product is created. + +```ts title="Workflow Hook Example" highlights={hookHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" +import { BRAND_MODULE } from "../../modules/brand" +import BrandModuleService from "../../modules/brand/service" + +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand_id) { + return new StepResponse([], []) + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brand = await brandModuleService.createBrands({ + name: additional_data.brand_name, + }) + }) +) +``` + +You can also build custom workflows using your custom module and Medusa's modules, and use [existing workflows and steps](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) within your custom workflows. + +### Examples + +The following tutorials are step-by-step guides that show you how to extend existing features using the Medusa Framework. + +- [Custom Item Pricing](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md): Add products with custom items to the cart. +- [Loyalty Points System](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points/index.html.md): Reward and allow customers to redeem loyalty points. + +### Start Learning + +To learn more about the different concepts useful for extending features, check out the following chapters: + +- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) + +*** + +## Integrate Third-Party Services + +The Medusa Framework provides the tools and infrastructure to build a middleware solution for your commerce ecosystem. You can integrate third-party services, perform operations across systems, and query data from multiple sources. + +### Orchestrate Operations Across Systems + +The Medusa Framework solves one of the biggest hurdles for ecommerce platforms: orchestrating operations across systems. Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. + +You can integrate a third-party service in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). This module provides an interface to perform operations with the third-party service. + +### Service + +```ts highlights={erpServiceHighlights} +type Options = { + apiKey: string +} + +export default class ErpModuleService { + private options: Options + private client + + constructor({}, options: Options) { + this.options = options + // TODO initialize client that connects to ERP + } + + async getProducts() { + // assuming client has a method to fetch products + return this.client.getProducts() + } + + // TODO add more methods +} +``` + +### Module Definition + +```ts +import { Module } from "@medusajs/framework/utils" +import ErpModuleService from "./service" + +export const ERP_MODULE = "erp" + +export default Module(ERP_MODULE, { + service: ErpModuleService, +}) +``` + +Then, you can build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that perform operations across systems. In the workflow, you can use your module to interact with the integrated third-party service. + +For example, you can create a workflow that syncs products from your ERP system to your Medusa application. + +### Workflow + +```ts highlights={erpWorkflowHighlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +export const syncFromErpWorkflow = createWorkflow( + "sync-from-erp", + () => { + const erpProducts = getProductsFromErpStep() + + const productsToCreate = transform({ + erpProducts, + }, (data) => { + // TODO prepare ERP products to be created in Medusa + return data.erpProducts.map((erpProduct) => { + return { + title: erpProduct.title, + external_id: erpProduct.id, + variants: erpProduct.variants.map((variant) => ({ + title: variant.title, + metadata: { + external_id: variant.id, + }, + })), + // other data... + } + }) + }) + + createProductsWorkflow.runAsStep({ + input: { + products: productsToCreate, + }, + }) + + return new WorkflowResponse({ + erpProducts, + }) + } +) +``` + +### Step + +```ts +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { ERP_MODULE } from "../../modules/erp" +import { ErpModuleService } from "../../modules/erp/service" + +const getProductsFromErpStep = createStep( + "get-products-from-erp", + async (_, { container }) => { + const erpModuleService: ErpModuleService = container.resolve( + ERP_MODULE + ) + + const products = await erpModuleService.getProducts() + + return new StepResponse(products) + } +) +``` + +By using a workflow to manage operations across systems, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md), [background long-running execution](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md), and more. This is essential for building a middleware solution that performs operations across systems, as you don't have to worry about data inconsistencies or failures. + +You can then execute this workflow at a specific interval using [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) or when an event occurs using [events and subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). You can also expose its features to client applications using an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). + +### Scheduled Job + +```ts highlights={syncProductsJobHighlights} +import { + MedusaContainer, +} from "@medusajs/framework/types" +import { syncFromErpWorkflow } from "../workflows/sync-from-erp" + +export default async function syncProductsJob(container: MedusaContainer) { + await syncFromErpWorkflow(container).run({}) +} + +export const config = { + name: "daily-product-sync", + schedule: "0 0 * * *", // Every day at midnight +} +``` + +### Event Subscriber + +```ts highlights={productsCreatedHandlerHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function productsCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }[]>) { + await syncFromErpWorkflow(container).run({}) +} + +export const config: SubscriberConfig = { + event: `product.created`, +} +``` + +### API Route + +```ts highlights={apiRouteHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { syncFromErpWorkflow } from "../../../workflows/sync-from-erp" + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await syncFromErpWorkflow(req.scope).run({}) + + return res.status(200).json(result) +} +``` + +#### Examples + +The following tutorials are step-by-step guides that show you how to orchestrate operations across third-party services using the Medusa Framework. + +- [Migrate Data from Magento](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Migrate data from Magento to your Medusa application. +- [Integrate Third-Party Services](https://docs.medusajs.com/resources/integrations/index.html.md): Integrate CMS, Fulfillment, Payment, and other third-party services. + +#### Start Learning + +To learn more about the different concepts useful for integrating third-party services, check out the following chapters: + +- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) + +### Query Data Across Systems + +Another essential feature for integrating third-party services is querying data across those systems efficiently. + +The Framework allows you to build links not only between Medusa data models, but also virtual data models using [read-only module links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). You can build a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that provides the logic to query data from a third-party service, then create a read-only link between an existing data model and a virtual one from the third-party service. + +### Read-Only Link + +```ts highlights={readOnlyLinkHighlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + field: "id", + }, + { + ...BrandModule.linkable.brand.id, + primaryKey: "product_id", + }, + { + readOnly: true, + } +) +``` + +### Module Service + +```ts highlights={brandModuleService} +type BrandModuleOptions = { + apiKey: string +} + +export default class BrandModuleService { + private client + + constructor({}, options: BrandModuleOptions) { + this.client = new Client(options) + } + + async list( + filter: { + id: string | string[] + } + ) { + return this.client.getBrands(filter) + /** + - Example of returned data: + - + - [ + - { + - "id": "brand_123", + - "name": "Brand 123", + - "product_id": "prod_321" + - }, + - { + - "id": "post_456", + - "name": "Brand 456", + - "product_id": "prod_654" + - } + - ] + */ + } +} +``` + +### Module Definition + +```ts +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" + +export const BRAND_MODULE = "brand" + +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` + +Then, you can use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve a product and its brand from the third-party service in a single query. + +```ts title="Query Example" highlights={queryHighlights} +const { result } = await query.graph({ + entity: "product", + fields: ["id", "brand.*"], + filters: { + id: "prod_123", + }, +}) + +// result = [{ +// id: "prod_123", +// brand: { +// id: "brand_123", +// name: "Brand 123", +// product_id: "prod_123" +// } +// ... +// }] +``` + +Query simplifies the process of retrieving data across systems, as you can retrieve data from multiple sources in a single query. + +#### Examples + +The following tutorials are step-by-step guides that show you how to query data across systems using the Medusa Framework. + +- [Integrate Sanity CMS](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md): Query data from third-party services using read-only links. + +#### Start Learning + +To learn more about the different concepts useful for querying data across systems, check out the following chapters: + +- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Read-Only Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) +- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) + +*** + +## Automate Tasks + +The Medusa Framework provides the tools to automate tasks in your Medusa application. Automation is useful when you want to perform a task periodically, such as syncing data, or when an event occurs, such as sending a confirmation email when an order is placed. + +To build the task to be automated, you first create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that contains the task's logic, such as syncing data or sending an email. + +### Step + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CreateNotificationDTO } from "@medusajs/framework/types" + +export const sendNotificationStep = createStep( + "send-notification", + async (data: CreateNotificationDTO[], { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + const notification = await notificationModuleService.createNotifications( + data + ) + return new StepResponse(notification) + } +) +``` + +### Workflow + +```ts +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { sendNotificationStep } from "./steps/send-notification" + +type WorkflowInput = { + id: string +} + +export const sendOrderConfirmationWorkflow = createWorkflow( + "send-order-confirmation", + ({ id }: WorkflowInput) => { + // @ts-ignore + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "email", + "currency_code", + "total", + "items.*", + ], + filters: { + id, + }, + }) + + const notification = sendNotificationStep([{ + to: orders[0].email, + channel: "email", + template: "order-placed", + data: { + order: orders[0], + }, + }]) + + return new WorkflowResponse(notification) + } +) +``` + +Then, you can execute this workflow when an event occurs using a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or at a specific interval using a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + +### Event Subscriber + +```ts highlights={orderPlacedHandlerHighlights} +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +### Scheduled Job + +```ts highlights={orderConfirmationJobHighlights} +import type { + MedusaContainer, +} from "@medusajs/framework/types" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function orderConfirmationJob( + container: MedusaContainer +) { + await sendOrderConfirmationWorkflow(container).run({ + input: { + id: "order_123", + }, + }) +} +export const config = { + name: "order-confirmation-job", + schedule: "0 0 * * *", // Every day at midnight +} +``` + +### Examples + +The following guides are step-by-step guides that show you how to automate tasks using the Medusa Framework. + +- [Restock Notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md): Send restock notifications to customers when a product is back in stock. +- [Sync Data from and to ERP](https://docs.medusajs.com/resources/recipes/erp#sync-products-from-erp/index.html.md): Sync data between your Medusa application and an ERP system. + +### Start Learning + +To learn more about the different concepts useful for automating tasks, check out the following chapters: + +- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) + +*** + +## Re-Use Customizations Across Applications + +If you have custom features that you want to re-use across multiple Medusa applications, or you want to publish your customizations for the community to use, you can build a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +A plugin encapsulates your customizations in a single package. The customizations include [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), and more. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + +You can then publish that plugin to NPM and install it in any Medusa application. This allows you to re-use your customizations efficiently across multiple projects, or share them with the community. + +### Examples + +The following tutorials are step-by-step guides that show you how to build plugins using the Medusa Framework. + +- [Wishlist Plugin](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist plugin for your Medusa application. +- [Migrate Data from Magento Plugin](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Build a plugin that migrates data from Magento to your Medusa application. + +### Start Learning + +To learn more about the different concepts useful for building plugins, check out the following chapters: + +- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) + + +# Medusa Container + +In this chapter, you’ll learn about the Medusa container and how to use it. + +## What is the Medusa Container? + +The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. + +In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. + +Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. + +For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: + +```ts highlights={highlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + + res.json({ + products, + }) +} +``` + +The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. + +You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +*** + +## List of Resources Registered in the Medusa Container + +Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) + +*** + +## How to Resolve From the Medusa Container + +This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. + +### Subscriber + +A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={subscriberHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: data.id, + }, + }) + + console.log(`You created a product with the title ${products[0].title}`) +} + +export const config: SubscriberConfig = { + event: `product.created`, +} +``` + +### Scheduled Job + +A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={scheduledJobHighlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + + console.log( + `You have ${products.length} matching your filters.` + ) +} + +export const config = { + name: "every-minute-message", + // execute every minute + schedule: "* * * * *", +} +``` + +### Workflow Step + +A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={workflowStepsHighlight} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +const step1 = createStep("step-1", async (_, { container }) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + + return new StepResponse(products) +}) +``` + +### Module Services and Loaders + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. + +Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). + + +# Modules + +In this chapter, you’ll learn about modules and how to create them. + +## What is a Module? + +A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. + +When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations. + +Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without affecting the existing setup. You can also re-use your modules across Medusa projects. + +As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem. + +- You want to build a custom feature related to a single domain or integrate a third-party service. + +- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +*** + +## How to Create a Module? + +In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module. + +In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post. + +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. + +### 1. Create Data Model + +A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: + +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) + +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) + +export default Post +``` + +You define the data model using the `define` method of the DML. It accepts two parameters: + +1. The first one is the name of the data model's table in the database. Use snake-case names. +2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. + - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. + +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#property-types/index.html.md). + +The code snippet above defines a `Post` data model with `id` and `title` properties. + +### 2. Create Service + +You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows. + +You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content: + +![Updated directory overview after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732807230/Medusa%20Book/blog-dir-overview-2_avzb9l.jpg) + +```ts title="src/modules/blog/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ +} + +export default BlogModuleService +``` + +Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving you time that can be spent on building custom business logic. + +The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object. + +For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model. + +Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) + +If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`. + +### 3. Export Module Definition + +The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name. + +So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content: + +![Updated directory overview after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808511/Medusa%20Book/blog-dir-overview-3_dcgjaa.jpg) + +```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights} +import BlogModuleService from "./service" +import { Module } from "@medusajs/framework/utils" + +export const BLOG_MODULE = "blog" + +export default Module(BLOG_MODULE, { + service: BlogModuleService, +}) +``` + +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The name that the module's main service is registered under (`blog`). The module name can contain only alphanumeric characters and underscores. +2. An object with a required property `service` indicating the module's main service. + +You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations. + +### 4. Add Module to Medusa's Configurations + +If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: + +```ts title="medusa-config.ts" highlights={[["7"]]} +module.exports = defineConfig({ + projectConfig: { + // ... + }, + modules: [ + { + resolve: "./src/modules/blog", + }, + ], +}) +``` + +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. + +### 5. Generate Migrations + +Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. + +Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change. + +To generate a migration for the Blog Module, run the following command in your Medusa application's directory: + +If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. + +```bash +npx medusa db:generate blog +``` + +The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: + +```ts +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241121103722 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"post\" cascade;") + } + +} +``` + +In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. + +### 6. Run Migrations + +To reflect the changes in the generated migration file on the database, run the `db:migrate` command: + +If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. + +```bash +npx medusa db:migrate +``` + +This creates the `post` table in the database. + +*** + +## Test the Module + +Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods. + +To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow. + +By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +To create the workflow, create the file `src/workflows/create-post.ts` with the following content: + +```ts title="src/workflows/create-post.ts" highlights={workflowHighlights} +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { BLOG_MODULE } from "../modules/blog" +import BlogModuleService from "../modules/blog/service" + +type CreatePostWorkflowInput = { + title: string +} + +const createPostStep = createStep( + "create-post", + async ({ title }: CreatePostWorkflowInput, { container }) => { + const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) + + const post = await blogModuleService.createPosts({ + title, + }) + + return new StepResponse(post, post) + }, + async (post, { container }) => { + const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) + + await blogModuleService.deletePosts(post.id) + } +) + +export const createPostWorkflow = createWorkflow( + "create-post", + (postInput: CreatePostWorkflowInput) => { + const post = createPostStep(postInput) + + return new WorkflowResponse(post) + } +) +``` + +The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`. + +The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution. + +You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content: + +```ts +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createPostWorkflow, +} from "../../../workflows/create-post" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const { result: post } = await createPostWorkflow(req.scope) + .run({ + input: { + title: "My Post", + }, + }) + + res.json({ + post, + }) +} +``` + +This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property. + +To test this out, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, send a `POST` request to `/blog/posts`: + +```bash +curl -X POST http://localhost:9000/blog/posts +``` + +This will create a post and return it in the response: + +```json +{ + "post": { + "id": "123...", + "title": "My Post", + "created_at": "...", + "updated_at": "..." + } +} +``` + +You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval. + + +# Plugins + +In this chapter, you'll learn what a plugin is in Medusa. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). + +*** + +## Plugin vs Module + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + +*** + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + + +# Define Module Link + +In this chapter, you’ll learn what a module link is and how to define one. + +## What is a Module Link? + +Medusa's modular architecture isolates modules from one another to ensure they can be integrated into your application without side effects. Module isolation has other benefits, which you can learn about in the [Module Isolation chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md). Since modules are isolated, you can't access another module's data models to add a relation to it or extend it. Instead, you use a module link. + +A module link forms an association between two data models of different modules while maintaining module isolation. Using module links, you can build virtual relations between your custom data models and data models in the Commerce Modules, which is useful as you extend the features provided by the Commerce Modules. Then, Medusa creates a link table in the database to store the IDs of the linked records. You'll learn more about link tables later in this chapter. + +For example, the [Brand Customizations Tutorial](https://docs.medusajs.com/learn/customization/extend-features/index.html.md) shows how to create a Brand Module that adds the concept of brands to your application, then link those brands to a product. + +*** + +## How to Define a Module Link? + +### 1. Create Link File + +Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using `defineLink` from the Modules SDK and exports it. + +For example: + +```ts title="src/links/blog-product.ts" highlights={highlights} +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.post +) +``` + +The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models. + +In this example, you define a module link between the `blog` module's `post` data model and the Product Module's `Product` data model. + +### 2. Sync Links + +After defining the link, run the `db:sync-links` command: + +```bash +npx medusa db:sync-links +``` + +The Medusa application creates a new table for your module link to store the IDs of linked records. + +You can also use the `db:migrate` command, which runs both the migrations and syncs the links. + +Use either of these commands whenever you make changes to your link definitions. For example, run this command if you remove your link definition file. + +*** + +### Module Link's Database Table + +When you define a module link, the Medusa application creates a table in the database for that module link. The table's name is a combination of the names of the two data models linked in the format `module1_table1_module2_table2`, where: + +- `module1` and `module2` are the names of the modules. +- `table1` and `table2` are the table names of the data models. + +For example, if you define a link between the `Product` data model from the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) and a `Post` data model from a Blog Module, the table name would be `product_product_blog_post`. + +The table has two columns, each storing the ID of a record from the linked data models. For example, the `product_product_blog_post` table would have columns `product_id` and `post_id`. These columns store only the IDs of the linked records and do not hold a foreign key constraint. + +Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table. + +You can also add custom columns in the link table as explained in the [Add Columns to Link Table chapter](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). + +![Diagram illustration for module links](https://res.cloudinary.com/dza7lstvk/image/upload/v1741696766/Medusa%20Book/custom-links_vezsx8.jpg) + +*** + +## When to Use Module Links + +- You want to create a relation between data models from different modules. +- You want to extend the data model of another module. + +You want to create a relationship between data models in the same module. Use data model relationships instead. + +*** + +## Define a List Module Link + +By default, a module link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model. + +To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option. + +For example: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + { + linkable: BlogModule.linkable.post, + isList: true, + } +) +``` + +In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties: + +- `linkable`: The data model's link configuration. +- `isList`: Whether multiple records can be linked to one record of the other data model. + +In this example, a record of `product` can be linked to more than one record of `post`. + +### Many-to-Many Module Link + +Your module link can also establish a many-to-many relation between the linked data models. To do this, enable `isList` on both sides of the link. + +For example: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + { + linkable: BlogModule.linkable.post, + isList: true, + } +) +``` + +*** + +## Set Delete Cascades on Link + +To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascade` property in the object passed to `defineLink`. + +For example: + +```ts +import BlogModule from "../modules/blog" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + { + linkable: BlogModule.linkable.post, + deleteCascade: true, + } +) +``` + +In this example, when a product is deleted, its linked `post` record is also deleted. + +*** + +## Renaming Participants in a Module Link + +As mentioned in the [Module Link's Database Table](#module-links-database-table) section, the name of a link's table consists of the names of the modules and the data models' table names. + +So, if you rename a module or a data model's table, then run the `db:sync-links` or `db:migrate` commands, you'll be asked to delete the old link table and create a new one. + +A data model's table name is passed in the first parameter of `model.define`, and a module's name is passed in the first parameter of `Module` in the module's `index.ts` file. + +For example, if you have the link table `product_product_blog_post` and you rename the Blog Module from `blog` to `article`, Medusa considers the old link definition deleted. Then, when you run the `db:sync-links` or `db:migrate` command, Medusa will ask if you want to delete the old link table, and will create a new one with the new name `product_product_article_post`. + +To resolve this, you can rename the link table in the link definition. + +### Rename Link Table + +If you need to rename a module or its data model's table, you can persist the old name by passing a third parameter to `defineLink`. This parameter is an object of additional configurations. It accepts a `database` property that allows you to configure the link's table name. + +For example, after renaming the Blog Module to `article`, you can persist the old name `blog` in the link table name: + +```ts highlights={renameHighlights} +import ArticleModule from "../modules/article" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + ProductModule.linkable.product, + { + linkable: ArticleModule.linkable.post, + isList: true, + }, + { + database: { + table: "product_product_blog_post", + }, + } +) +``` + +In this example, you set the `table` property in the `database` object to the old link table name `product_product_blog_post`, ensuring that the old link table is not deleted. + +This is enough to rename the link table when you rename a module. If you renamed a data model's table, you need to also run the `db:sync-links` or `db:migrate` commands, which will update the column names in the link table automatically: + +```bash +npx medusa db:migrate +``` + +*** + +## Delete Module Link Definition + +To delete a module link definition, remove the link file from the `src/links` directory. Then, run the `db:sync-links` or `db:migrate` command to delete the link table from the database: + +```bash +npx medusa db:migrate +``` + + +# Workflows + +In this chapter, you’ll learn about workflows and how to define and execute them. + +## What is a Workflow? + +In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems. + +Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function. + +However, unlike regular functions, workflows: + +- Create an internal representation of your steps, allowing you to track them and their progress. +- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems. +- Perform long actions asynchronously, giving you control over when a step starts and finishes. + +You implement all custom flows within workflows, then execute them from [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), and [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + +*** + +## How to Create and Execute a Workflow? + +### 1. Create the Steps + +A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK. + +Create the file `src/workflows/hello-world.ts` with the following content: + +![Example of workflow file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866980/Medusa%20Book/workflow-dir-overview_xklukj.jpg) + +```ts title="src/workflows/hello-world.ts" highlights={step1Highlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + return new StepResponse(`Hello from step one!`) + } +) +``` + +The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter. + +Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step. + +Steps can accept input parameters. For example, add the following to `src/workflows/hello-world.ts`: + +```ts title="src/workflows/hello-world.ts" highlights={step2Highlights} +type WorkflowInput = { + name: string +} + +const step2 = createStep( + "step-2", + async ({ name }: WorkflowInput) => { + return new StepResponse(`Hello ${name} from step two!`) + } +) +``` + +This adds another step whose function accepts as a parameter an object with a `name` property. + +### 2. Create a Workflow + +Next, add the following to the same file to create the workflow using the `createWorkflow` function: + +```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights} +import { + // other imports... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1() + // to pass input + const str2 = step2(input) + + return new WorkflowResponse({ + message: str2, + }) + } +) + +export default myWorkflow +``` + +The `createWorkflow` function accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The workflow can accept input which is passed as a parameter to the function. + +The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors. + +### 3. Execute the Workflow + +You can execute a workflow from different customizations: + +- Execute in an API route to expose the workflow's functionalities to clients. +- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed. +- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval. + +To execute the workflow, invoke it passing the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. Then, use its `run` method: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../workflows/hello-world" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run({ + input: { + name: "John", + }, + }) + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/order-placed.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import myWorkflow from "../workflows/hello-world" + +export default async function handleOrderPlaced({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +### Scheduled Job + +```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import myWorkflow from "../workflows/hello-world" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) + + console.log(result.message) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +}; +``` + +### 4. Test Workflow + +To test out your workflow, start your Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, if you added the API route above, send a `GET` request to `/workflow`: + +```bash +curl http://localhost:9000/workflow +``` + +You’ll receive the following response: + +```json title="Example Response" +{ + "message": "Hello John from step two!" +} +``` + +*** + +## Access Medusa Container in Workflow Steps + +A step receives an object as a second parameter with configurations and context-related properties. One of these properties is the `container` property, which is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to allow you to resolve Framework and commerce tools in your application. + +For example, consider you want to implement a workflow that returns the total products in your application. Create the file `src/workflows/product-count.ts` with the following content: + +```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const getProductCountStep = createStep( + "get-product-count", + async (_, { container }) => { + const productModuleService = container.resolve("product") + + const [, count] = await productModuleService.listAndCountProducts() + + return new StepResponse(count) + } +) + +const productCountWorkflow = createWorkflow( + "product-count", + function () { + const count = getProductCountStep() + + return new WorkflowResponse({ + count, + }) + } +) + +export default productCountWorkflow +``` + +In `getProductCountStep`, you use the `container` to resolve the Product Module's main service. Then, you use its `listAndCountProducts` method to retrieve the total count of products and return it in the step's response. You then execute this step in the `productCountWorkflow`. + +You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products. + +Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows. + + # Medusa Application Configuration In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more. @@ -1420,135 +4082,6 @@ import { BrandModuleService } from "@/modules/brand/service" ``` -# Customize Medusa Admin Dashboard - -In the previous chapters, you've customized your Medusa application to [add brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), [expose an API route to create brands](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), and [linked brands to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md). - -After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: - -- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. -- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). - -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard - -*** - -## Next Chapters: View Brands in Dashboard - -In the next chapters, you'll continue with the brands example to: - -- Add a new section to the product details page that shows the product's brand. -- Add a new page in the dashboard that shows all brands in the store. - - -# Build Custom Features - -In the upcoming chapters, you'll follow step-by-step guides to build custom features in Medusa. These guides gradually introduce Medusa's concepts to help you understand what they are and how to use them. - -By following these guides, you'll add brands to the Medusa application that you can associate with products. - -To build a custom feature in Medusa, you need three main tools: - -- [Module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. -- [Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. -- [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. - -![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) - -*** - -## Next Chapters: Brand Module Example - -The next chapters will guide you to: - -1. Build a Brand Module that creates a `Brand` data model and provides data-management features. -2. Add a workflow to create a brand. -3. Expose an API route that allows admin users to create a brand using the workflow. - - -# Extend Core Commerce Features - -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. - -In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run. - -The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: - -- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. -- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. -- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. - -*** - -## Next Chapters: Link Brands to Products Example - -The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to: - -- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). -- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product. -- Retrieve a product's associated brand's details. - - -# Integrate Third-Party Systems - -Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. - -The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. - -In Medusa, you integrate a third-party system by: - -1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. -2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. -3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. - -*** - -## Next Chapters: Sync Brands Example - -In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: - -1. Integrate a dummy third-party CMS in the Brand Module. -2. Sync brands to the CMS when a brand is created. -3. Sync brands from the CMS at a daily schedule. - - -# Customizations Next Steps: Learn the Fundamentals - -The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. - -The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. - -## Useful Guides - -The following guides and references are useful for your development journey: - -3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them. -4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. -5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. -6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. - -*** - -## More Examples in Recipes - -In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. - - -# Re-Use Customizations with Plugins - -In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. - -You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. - -To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. - -![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) - -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. - -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - # General Medusa Application Deployment Guide In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. @@ -1857,2245 +4390,77 @@ Replace the email `admin-medusa@test.com` and password `supersecret` with the cr You can use these credentials to log into the Medusa Admin dashboard. -# Configure Instrumentation +# Medusa's Architecture -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. +In this chapter, you'll learn about the architectural layers in Medusa. -## Observability with OpenTelemtry +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: +## HTTP, Workflow, and Module Layers -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. + +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: + +1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. + +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) *** -## How to Configure Instrumentation in Medusa? +## Database Layer -### Prerequisites +The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -### Install Dependencies - -Start by installing the following OpenTelemetry dependencies in your Medusa project: - -```bash npm2yarn -npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg -``` - -Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: - -```bash npm2yarn -npm install @opentelemetry/exporter-zipkin -``` - -### Add instrumentation.ts - -Next, create the file `instrumentation.ts` with the following content: - -```ts title="instrumentation.ts" -import { registerOtel } from "@medusajs/medusa" -import { ZipkinExporter } from "@opentelemetry/exporter-zipkin" - -// If using an exporter other than Zipkin, initialize it here. -const exporter = new ZipkinExporter({ - serviceName: "my-medusa-project", -}) - -export function register() { - registerOtel({ - serviceName: "medusajs", - // pass exporter - exporter, - instrument: { - http: true, - workflows: true, - query: true, - }, - }) -} -``` - -In the `instrumentation.ts` file, you export a `register` function that uses Medusa's `registerOtel` utility function. You also initialize an instance of the exporter, such as Zipkin, and pass it to the `registerOtel` function. - -`registerOtel` accepts an object where you can pass any [NodeSDKConfiguration](https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_sdk_node.NodeSDKConfiguration.html) property along with the following properties: - -The `NodeSDKConfiguration` properties are accepted since Medusa v2.5.1. - -- serviceName: (\`string\`) The name of the service traced. -- exporter: (\[SpanExporter]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_sdk\_trace\_base.SpanExporter.html)) An instance of an exporter, such as Zipkin. -- instrument: (\`object\`) Options specifying what to trace. - - - http: (\`boolean\`) Whether to trace HTTP requests. - - - query: (\`boolean\`) Whether to trace Query usages. - - - workflows: (\`boolean\`) Whether to trace Workflow executions. - - - db: (\`boolean\`) Whether to trace database queries and operations. -- instrumentations: (\[Instrumentation\[]]\(https://open-telemetry.github.io/opentelemetry-js/interfaces/\_opentelemetry\_instrumentation.Instrumentation.html)) Additional instrumentation options that OpenTelemetry accepts. +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) *** -## Test it Out +## Third-Party Integrations Layer -To test it out, start your exporter, such as Zipkin. +Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -Then, start your Medusa application: +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -```bash npm2yarn -npm run dev -``` +### Commerce Modules -Try to open the Medusa Admin or send a request to an API route. +[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. -If you check traces in your exporter, you'll find new traces reported. +You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. -### Trace Span Names +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. -Trace span names start with the following keywords based on what it's reporting: +![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) -- `{methodName} {URL}` when reporting HTTP requests, where `{methodName}` is the HTTP method, and `{URL}` is the URL the request is sent to. -- `route:` when reporting route handlers running on an HTTP request. -- `middleware:` when reporting a middleware running on an HTTP request. -- `workflow:` when reporting a workflow execution. -- `step:` when reporting a step in a workflow execution. -- `query.graph:` when reporting Query usages. -- `pg.query:` when reporting database queries and operations. +### Infrastructure Modules +[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: -# Logging +- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). +- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. +- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. +- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. +- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. +- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. -In this chapter, you’ll learn how to use Medusa’s logging utility. +All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. -## Logger Class - -Medusa provides a `Logger` class with advanced logging functionalities. This includes configuring logging levels or saving logs to a file. - -The Medusa application registers the `Logger` class in the Medusa container and each module's container as `logger`. +![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) *** -## How to Log a Message +## Full Diagram of Medusa's Architecture -Resolve the `logger` using the Medusa container to log a message in your resource. +The following diagram illustrates Medusa's architecture including all its layers. -For example, create the file `src/jobs/log-message.ts` with the following content: - -```ts title="src/jobs/log-message.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - logger.info("I'm using the logger!") -} - -export const config = { - name: "test-logger", - // execute every minute - schedule: "* * * * *", -} -``` - -This creates a scheduled job that resolves the `logger` from the Medusa container and uses it to log a message. - -### Test the Scheduled Job - -To test out the above scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, you'll see the following message as part of the logged messages: - -```text -info: I'm using the logger! -``` - -*** - -## Log Levels - -The `Logger` class has the following methods: - -- `info`: The message is logged with level `info`. -- `warn`: The message is logged with level `warn`. -- `error`: The message is logged with level `error`. -- `debug`: The message is logged with level `debug`. - -Each of these methods accepts a string parameter to log in the terminal with the associated level. - -*** - -## Logging Configurations - -### Log Level - -The available log levels, from lowest to highest levels, are: - -1. `silly` (default, meaning messages of all levels are logged) -2. `debug` -3. `info` -4. `warn` -5. `error` - -You can change that by setting the `LOG_LEVEL` environment variable to the minimum level you want to be logged. - -For example: - -```bash -LOG_LEVEL=error -``` - -This logs `error` messages only. - -The environment variable must be set as a system environment variable and not in `.env`. - -### Save Logs in a File - -Aside from showing the logs in the terminal, you can save the logs in a file by setting the `LOG_FILE` environment variable to the path of the file relative to the Medusa server’s root directory. - -For example: - -```bash -LOG_FILE=all.log -``` - -Your logs are now saved in the `all.log` file at the root of your Medusa application. - -The environment variable must be set as a system environment variable and not in `.env`. - -*** - -## Show Log with Progress - -The `Logger` class has an `activity` method used to log a message of level `info`. If the Medusa application is running in a development environment, a spinner starts to show the activity's progress. - -For example: - -```ts title="src/jobs/log-message.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - const activityId = logger.activity("First log message") - - logger.progress(activityId, `Second log message`) - - logger.success(activityId, "Last log message") -} -``` - -The `activity` method returns the ID of the started activity. This ID can then be passed to one of the following methods of the `Logger` class: - -- `progress`: Log a message of level `info` that indicates progress within that same activity. -- `success`: Log a message of level `info` that indicates that the activity has succeeded. This also ends the associated activity. -- `failure`: Log a message of level `error` that indicates that the activity has failed. This also ends the associated activity. - -If you configured the `LOG_LEVEL` environment variable to a level higher than those associated with the above methods, their messages won’t be logged. - - -# Medusa Testing Tools - -In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. - -## @medusajs/test-utils Package - -Medusa provides a Testing Framework to create integration tests for your custom API routes, modules, or other Medusa customizations. - -To use the Testing Framework, install `@medusajs/test-utils` as a `devDependency`: - -```bash npm2yarn -npm install --save-dev @medusajs/test-utils@latest -``` - -*** - -## Install and Configure Jest - -Writing tests with `@medusajs/test-utils`'s tools requires installing and configuring Jest in your project. - -Run the following command to install the required Jest dependencies: - -```bash npm2yarn -npm install --save-dev jest @types/jest @swc/jest -``` - -Then, create the file `jest.config.js` with the following content: - -```js title="jest.config.js" -const { loadEnv } = require("@medusajs/framework/utils") -loadEnv("test", process.cwd()) - -module.exports = { - transform: { - "^.+\\.[jt]s$": [ - "@swc/jest", - { - jsc: { - parser: { syntax: "typescript", decorators: true }, - }, - }, - ], - }, - testEnvironment: "node", - moduleFileExtensions: ["js", "ts", "json"], - modulePathIgnorePatterns: ["dist/"], - setupFiles: ["./integration-tests/setup.js"], -} - -if (process.env.TEST_TYPE === "integration:http") { - module.exports.testMatch = ["**/integration-tests/http/*.spec.[jt]s"] -} else if (process.env.TEST_TYPE === "integration:modules") { - module.exports.testMatch = ["**/src/modules/*/__tests__/**/*.[jt]s"] -} else if (process.env.TEST_TYPE === "unit") { - module.exports.testMatch = ["**/src/**/__tests__/**/*.unit.spec.[jt]s"] -} -``` - -Next, create the `integration-tests/setup.js` file with the following content: - -```js title="integration-tests/setup.js" -const { MetadataStorage } = require("@mikro-orm/core") - -MetadataStorage.clear() -``` - -*** - -## Add Test Commands - -Finally, add the following scripts to `package.json`: - -```json title="package.json" -"scripts": { - // ... - "test:integration:http": "TEST_TYPE=integration:http NODE_OPTIONS=--experimental-vm-modules jest --silent=false --runInBand --forceExit", - "test:integration:modules": "TEST_TYPE=integration:modules NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit", - "test:unit": "TEST_TYPE=unit NODE_OPTIONS=--experimental-vm-modules jest --silent --runInBand --forceExit" -}, -``` - -You now have two commands: - -- `test:integration:http` to run integration tests (for example, for API routes and workflows) available under the `integration-tests/http` directory. -- `test:integration:modules` to run integration tests for modules available in any `__tests__` directory under `src/modules`. -- `test:unit` to run unit tests in any `__tests__` directory under the `src` directory. - -Medusa's Testing Framework works for integration tests only. You can write unit tests using Jest. - -*** - -## Test Tools and Writing Tests - -The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. - - -# Admin Development - -In this chapter, you'll learn about th Medusa Admin dashboard and the possible ways to customize it. - -## What is the Medusa Admin? - -The Medusa Admin is an intuitive dashboard that allows merchants to manage their ecommerce store. It provides management featuers related to products, orders, customers, and more. - -To explore more what you can do with the Medusa Admin, check out the [User Guide](https://docs.medusajs.com/user-guide/index.html.md). These user guides are designed for merchants and provide the steps to perform any task within the Medusa Admin. - -The Medusa Admin is built with [Vite](https://vite.dev/). When you [install the Medusa application](https://docs.medusajs.com/learn/installation/index.html.md), you also install the Medusa Admin. Then, when you start the Medusa application, you can access the Medusa Admin at `http://localhost:9000/app`. - -If you don't have an admin user, use the [Medusa CLI](https://docs.medusajs.com/resources/medusa-cli/commands/user/index.html.md) to create one. - -*** - -## How to Customize the Medusa Admin? - -You can customize the Medusa Admin dashboard by: - -- Adding new sections to existing pages using [Widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). -- Adding new pages using [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). - -The next chapters will cover these two topics in detail. - -### What You Can't Customize in the Medusa Admin - -You can't customize the admin dashboard's layout, design, or the content of the existing pages (aside from injecting widgets). - -If your use case requires heavy customization of the admin dashboard, you can build a custom admin dashboard using Medusa's [Admin API routes](https://docs.medusajs.com/api/admin). - -*** - -## Medusa UI Package - -Medusa provides a Medusa UI package to facilitate your admin development through ready-made components and ensure a consistent design between your customizations and the dashboard’s design. - -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components. - -*** - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - -# API Routes - -In this chapter, you’ll learn what API Routes are and how to create them. - -## What is an API Route? - -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. - -The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. - -*** - -## How to Create an API Route? - -An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. - -![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) - -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). - -For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} -``` - -### Test API Route - -To test the API route above, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `GET` request to the `/hello-world` API Route: - -```bash -curl http://localhost:9000/hello-world -``` - -*** - -## When to Use API Routes - -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. - - -# Custom CLI Scripts - -In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. - -## What is a Custom CLI Script? - -A custom CLI script is a function to execute through Medusa's CLI tool. This is useful when creating custom Medusa tooling to run through the CLI. - -*** - -## How to Create a Custom CLI Script? - -To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. - -For example, create the file `src/scripts/my-script.ts` with the following content: - -```ts title="src/scripts/my-script.ts" -import { - ExecArgs, - IProductModuleService, -} from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" - -export default async function myScript({ container }: ExecArgs) { - const productModuleService: IProductModuleService = container.resolve( - Modules.PRODUCT - ) - - const [, count] = await productModuleService - .listAndCountProducts() - - console.log(`You have ${count} product(s)`) -} -``` - -The function receives as a parameter an object having a `container` property, which is an instance of the Medusa Container. Use it to resolve resources in your Medusa application. - -*** - -## How to Run Custom CLI Script? - -To run the custom CLI script, run the Medusa CLI's `exec` command: - -```bash -npx medusa exec ./src/scripts/my-script.ts -``` - -*** - -## Custom CLI Script Arguments - -Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. - -For example: - -```ts -import { ExecArgs } from "@medusajs/framework/types" - -export default async function myScript({ args }: ExecArgs) { - console.log(`The arguments you passed: ${args}`) -} -``` - -Then, pass the arguments in the `exec` command after the file path: - -```bash -npx medusa exec ./src/scripts/my-script.ts arg1 arg2 -``` - - -# Environment Variables - -In this chapter, you'll learn how environment variables are loaded in Medusa. - -## System Environment Variables - -The Medusa application loads and uses system environment variables. - -For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. - -In production, you should always use system environment variables that you set through your hosting provider. - -*** - -## Environment Variables in .env Files - -During development, it's easier to set environment variables in a `.env` file in your repository. - -Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: - -As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. - -|\`.env\`| -|---|---| -|\`NODE\_ENV\`|\`.env\`| -|\`NODE\_ENV\`|\`.env.production\`| -|\`NODE\_ENV\`|\`.env.staging\`| -|\`NODE\_ENV\`|\`.env.test\`| - -### Set Environment in `loadEnv` - -In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. - -This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. - -To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. - -*** - -## Environment Variables for Admin Customizations - -Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -*** - -## Predefined Medusa Environment Variables - -The Medusa application uses the following predefined environment variables that you can set: - -You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). - -|Environment Variable|Description|Default| -|---|---|---|---|---| -| -| -| -| -||The URL to connect to the PostgreSQL database. Only used if || -||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || -||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || -||URLs of clients that can access the Medusa backend's authentication routes. Only used if || -||A random string used to create authentication tokens in the http layer. Only used if || -||A random string used to create cookie tokens in the http layer. Only used if || -||The URL to the Medusa backend. Only used if || -| -| -| -| -| -| -| -| -||The allowed levels to log. Learn more in || -||The file to save logs in. By default, logs aren't saved in any file. Learn more in || -||Whether to disable analytics data collection. Learn more in || - - -# Events and Subscribers - -In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. - -## Handle Core Commerce Flows with Events - -When building commerce digital applications, you'll often need to perform an action after a commerce operation is performed. For example, sending an order confirmation email when the customer places an order, or syncing data that's updated in Medusa to a third-party system. - -Medusa emits events when core commerce features are performed, and you can listen to and handle these events in asynchronous functions. You can think of Medusa's events like you'd think about webhooks in other commerce platforms, but instead of having to setup separate applications to handle webhooks, your efforts only go into writing the logic right in your Medusa codebase. - -You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. - -![A diagram showcasing an example of how an event is emitted when an order is placed.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732277948/Medusa%20Book/order-placed-event-example_e4e4kw.jpg) - -Subscribers are useful to perform actions that aren't integral to the original flow. For example, you can handle the `order.placed` event in a subscriber that sends a confirmation email to the customer. The subscriber has no impact on the original order-placement flow, as it's executed outside of it. - -If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) instead. - -### List of Emitted Events - -Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/references/events/index.html.md). - -*** - -## How to Create a Subscriber? - -You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. The file exports the function to execute and the subscriber's configuration that indicate what event(s) it listens to. - -For example, create the file `src/subscribers/order-placed.ts` with the following content: - -![Example of subscriber file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866244/Medusa%20Book/subscriber-dir-overview_pusyeu.jpg) - -```ts title="src/subscribers/product-created.ts" -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" - -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const logger = container.resolve("logger") - - logger.info("Sending confirmation email...") - - await sendOrderConfirmationWorkflow(container) - .run({ - input: { - id: data.id, - }, - }) -} - -export const config: SubscriberConfig = { - event: `order.placed`, -} -``` - -This subscriber file exports: - -- An asynchronous subscriber function that's executed whenever the associated event, which is `order.placed` is triggered. -- A configuration object with an `event` property whose value is the event the subscriber is listening to. You can also pass an array of event names to listen to multiple events in the same subscriber. - -The subscriber function receives an object as a parameter that has the following properties: - -- `event`: An object with the event's details. The `data` property contains the data payload of the event emitted, which is the order's ID in this case. -- `container`: The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that you can use to resolve registered resources. - -In the subscriber function, you use the container to resolve the Logger utility and log a message in the console. Also, assuming you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that sends an order confirmation email, you execute it in the subscriber. - -*** - -## Test the Subscriber - -To test the subscriber, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, try placing an order either using Medusa's API routes or the [Next.js Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md). You'll see the following message in the terminal: - -```bash -info: Processing order.placed which has 1 subscribers -Sending confirmation email... -``` - -The first message indicates that the `order.placed` event was emitted, and the second one is the message logged from the subscriber. - -*** - -## Event Module - -The subscription and emitting of events is handled by an Event Module, an Infrastructure Module that implements the pub/sub functionalities of Medusa's event system. - -Medusa provides two Event Modules out of the box: - -- [Local Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/local/index.html.md), used by default. It's useful for development, as you don't need additional setup to use it. -- [Redis Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system. - -Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md). - - -# Data Models - -In this chapter, you'll learn what a data model is and how to create a data model. - -## What is a Data Model? - -A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. - -You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. - -*** - -## How to Create a Data Model - -In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. - -So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: - -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) - -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) - -export default Post -``` - -You define the data model using the `define` method of the DML. It accepts two parameters: - -1. The first one is the name of the data model's table in the database. Use snake-case names. -2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. - - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. - -The code snippet above defines a `Post` data model with `id` and `title` properties. - -*** - -## Generate Migrations - -After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: - -If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. - -```bash -npx medusa db:generate blog -``` - -The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: - -```ts -import { Migration } from "@mikro-orm/migrations" - -export class Migration20241121103722 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"post\" cascade;") - } - -} -``` - -In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. - -### Run Migrations - -To reflect the changes in the generated migration file on the database, run the `db:migrate` command: - -If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. - -```bash -npx medusa db:migrate -``` - -This creates the `post` table in the database. - -### Migrations on Data Model Changes - -Whenever you make a change to a data model, you must generate and run the migrations. - -For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. - -*** - -## Manage Data Models - -Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. - -For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. - -Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. - - -# Framework Overview - -In this chapter, you'll learn about the Medusa Framework and how it facilitates building customizations in your Medusa application. - -## What is the Medusa Framework? - -All commerce application require some degree of customization. So, it's important to choose a platform that facilitates building those customizations. - -When you build customizations with other ecommerce platforms, they require you to pull data through HTTP APIs, run custom logic that span across systems in a separate application, and manually ensure data consistency across systems. This adds significant overhead and slows down development as you spend time managing complex distributed systems. - -The Medusa Framework eliminates this overhead by providing powerful low-level APIs and tools that let you build any type of customization directly within your Medusa project. You can build custom features, orchestrate operations and query data seamlessy across systems, extend core functionality, and automate tasks in your Medusa application. - -With the Medusa Framework, you can focus your efforts on building meaningful business customizations and continuously delivering new features. - -Using the Medusa Framework, you can build customizations like: - -- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md) -- [Deep integration with an ERP system](https://docs.medusajs.com/resources/recipes/erp/index.html.md) -- [CMS integration with seamless content retrieval](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) -- [Custom item pricing in the cart](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md) -- [Automated restock notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md) -- [Re-usable payment provider integrations](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) - -### Framework Concepts and Tools - -- [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) -- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) -- [Data Models](https://docs.medusajs.com/learn/fundamentals/data-models/index.html.md) -- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) - -*** - -## Build Custom Features - -The Medusa Framework allows you to build custom features tailored to your business needs. - -To create a custom feature, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that contains your feature's data models and the logic to manage them. A module is integrated into your Medusa application without side effects. - -### Data Model - -```ts highlights={modelHighlights} -import { model } from "@medusajs/framework/utils" - -export const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) -``` - -### Service - -```ts highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Post } from "./post" - -export class BlogModuleService extends MedusaService({ - Post, -}){ - // CRUD methods generated by MedusaService -} -``` - -### Module Definition - -```ts -import { Module } from "@medusajs/framework/utils" -import { BlogModuleService } from "./service" - -export const BLOG_MODULE = "blog" - -export default Module(BLOG_MODULE, { - service: BlogModuleService, -}) -``` - -Then, you can build commerce features and flows in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that use your module. By using workflows, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md) and [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). - -### Step - -```ts highlights={stepHighlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { BlogModuleService, BLOG_MODULE } from "../../modules/blog" - -type Input = { - title: string -} - -const createPostStep = createStep( - "create-post", - async (input: Input, { container }) => { - const blogModuleService: BlogModuleService = container.resolve( - BLOG_MODULE - ) - - const post = await blogModuleService.createPosts(input.title) - - return new StepResponse(post, post.id) - }, - async (postId, { container }) => { - if (!postId) { - return - } - - const blogModuleService: BlogModuleService = container.resolve( - BLOG_MODULE - ) - - await blogModuleService.deletePosts(postId) - } -) -``` - -### Workflow - -```ts -import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" -import { createPostStep } from "./steps" - -type Input = { - title: string -} - -export const createPostWorkflow = createWorkflow( - "create-post", - (input: Input) => { - const post = createPostStep(input) - - return new WorkflowResponse(post) - } -) -``` - -Finally, you can expose your custom feature with [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are built on top of your module and workflows. - -```ts title="API Route Example" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPostWorkflow } from "../../../workflows/create-post" - -type PostRequestBody = { - title: string -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createPostWorkflow(req.scope) - .run({ - input: result.validatedBody, - }) - - return res.json(result) -} -``` - -### Examples - -The following tutorials are step-by-step guides that show you how to build custom features using the Medusa Framework. - -- [Product Reviews](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews/index.html.md): Build a product reviews feature in your Medusa application. -- [Wishlist](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist feature in your Medusa application. - -### Start Learning - -To learn more about the different concepts useful for building custom features, check out the following chapters: - -- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) - -*** - -## Extend Existing Features - -The Medusa Framework is flexible and extensible, allowing you to extend and build on top of existing models and features. - -To associate new properties and relations with an existing model, you can create a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) with data models that define these additions. Then, you can define a [module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that associates two data models from separate modules. - -### Module Link - -```ts highlights={defineLinkHighlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -### Data Model - -```ts -import { model } from "@medusajs/framework/utils" - -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) -``` - -### Service - -```ts -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" - -class BrandModuleService extends MedusaService({ - Brand, -}) { - -} - -export default BrandModuleService -``` - -### Module Definition - -```ts -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" - -export const BRAND_MODULE = "brand" - -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) -``` - -Then, you can [hook into existing workflows](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) to perform custom actions as part of existing features and flows. For example, you can create a brand when a product is created. - -```ts title="Workflow Hook Example" highlights={hookHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { LinkDefinition } from "@medusajs/framework/types" -import { BRAND_MODULE } from "../../modules/brand" -import BrandModuleService from "../../modules/brand/service" - -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand_id) { - return new StepResponse([], []) - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const brand = await brandModuleService.createBrands({ - name: additional_data.brand_name, - }) - }) -) -``` - -You can also build custom workflows using your custom module and Medusa's modules, and use [existing workflows and steps](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) within your custom workflows. - -### Examples - -The following tutorials are step-by-step guides that show you how to extend existing features using the Medusa Framework. - -- [Custom Item Pricing](https://docs.medusajs.com/resources/examples/guides/custom-item-price/index.html.md): Add products with custom items to the cart. -- [Loyalty Points System](https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points/index.html.md): Reward and allow customers to redeem loyalty points. - -### Start Learning - -To learn more about the different concepts useful for extending features, check out the following chapters: - -- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) - -*** - -## Integrate Third-Party Services - -The Medusa Framework provides the tools and infrastructure to build a middleware solution for your commerce ecosystem. You can integrate third-party services, perform operations across systems, and query data from multiple sources. - -### Orchestrate Operations Across Systems - -The Medusa Framework solves one of the biggest hurdles for ecommerce platforms: orchestrating operations across systems. Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. - -You can integrate a third-party service in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). This module provides an interface to perform operations with the third-party service. - -### Service - -```ts highlights={erpServiceHighlights} -type Options = { - apiKey: string -} - -export default class ErpModuleService { - private options: Options - private client - - constructor({}, options: Options) { - this.options = options - // TODO initialize client that connects to ERP - } - - async getProducts() { - // assuming client has a method to fetch products - return this.client.getProducts() - } - - // TODO add more methods -} -``` - -### Module Definition - -```ts -import { Module } from "@medusajs/framework/utils" -import ErpModuleService from "./service" - -export const ERP_MODULE = "erp" - -export default Module(ERP_MODULE, { - service: ErpModuleService, -}) -``` - -Then, you can build [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that perform operations across systems. In the workflow, you can use your module to interact with the integrated third-party service. - -For example, you can create a workflow that syncs products from your ERP system to your Medusa application. - -### Workflow - -```ts highlights={erpWorkflowHighlights} -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -export const syncFromErpWorkflow = createWorkflow( - "sync-from-erp", - () => { - const erpProducts = getProductsFromErpStep() - - const productsToCreate = transform({ - erpProducts, - }, (data) => { - // TODO prepare ERP products to be created in Medusa - return data.erpProducts.map((erpProduct) => { - return { - title: erpProduct.title, - external_id: erpProduct.id, - variants: erpProduct.variants.map((variant) => ({ - title: variant.title, - metadata: { - external_id: variant.id, - }, - })), - // other data... - } - }) - }) - - createProductsWorkflow.runAsStep({ - input: { - products: productsToCreate, - }, - }) - - return new WorkflowResponse({ - erpProducts, - }) - } -) -``` - -### Step - -```ts -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { ERP_MODULE } from "../../modules/erp" -import { ErpModuleService } from "../../modules/erp/service" - -const getProductsFromErpStep = createStep( - "get-products-from-erp", - async (_, { container }) => { - const erpModuleService: ErpModuleService = container.resolve( - ERP_MODULE - ) - - const products = await erpModuleService.getProducts() - - return new StepResponse(products) - } -) -``` - -By using a workflow to manage operations across systems, you benefit from features like [rollback mechanism](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md), [background long-running execution](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), [retry configuration](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md), and more. This is essential for building a middleware solution that performs operations across systems, as you don't have to worry about data inconsistencies or failures. - -You can then execute this workflow at a specific interval using [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) or when an event occurs using [events and subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). You can also expose its features to client applications using an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). - -### Scheduled Job - -```ts highlights={syncProductsJobHighlights} -import { - MedusaContainer, -} from "@medusajs/framework/types" -import { syncFromErpWorkflow } from "../workflows/sync-from-erp" - -export default async function syncProductsJob(container: MedusaContainer) { - await syncFromErpWorkflow(container).run({}) -} - -export const config = { - name: "daily-product-sync", - schedule: "0 0 * * *", // Every day at midnight -} -``` - -### Event Subscriber - -```ts highlights={productsCreatedHandlerHighlights} -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" - -export default async function productsCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }[]>) { - await syncFromErpWorkflow(container).run({}) -} - -export const config: SubscriberConfig = { - event: `product.created`, -} -``` - -### API Route - -```ts highlights={apiRouteHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { syncFromErpWorkflow } from "../../../workflows/sync-from-erp" - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await syncFromErpWorkflow(req.scope).run({}) - - return res.status(200).json(result) -} -``` - -#### Examples - -The following tutorials are step-by-step guides that show you how to orchestrate operations across third-party services using the Medusa Framework. - -- [Migrate Data from Magento](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Migrate data from Magento to your Medusa application. -- [Integrate Third-Party Services](https://docs.medusajs.com/resources/integrations/index.html.md): Integrate CMS, Fulfillment, Payment, and other third-party services. - -#### Start Learning - -To learn more about the different concepts useful for integrating third-party services, check out the following chapters: - -- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) - -### Query Data Across Systems - -Another essential feature for integrating third-party services is querying data across those systems efficiently. - -The Framework allows you to build links not only between Medusa data models, but also virtual data models using [read-only module links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). You can build a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that provides the logic to query data from a third-party service, then create a read-only link between an existing data model and a virtual one from the third-party service. - -### Read-Only Link - -```ts highlights={readOnlyLinkHighlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - field: "id", - }, - { - ...BrandModule.linkable.brand.id, - primaryKey: "product_id", - }, - { - readOnly: true, - } -) -``` - -### Module Service - -```ts highlights={brandModuleService} -type BrandModuleOptions = { - apiKey: string -} - -export default class BrandModuleService { - private client - - constructor({}, options: BrandModuleOptions) { - this.client = new Client(options) - } - - async list( - filter: { - id: string | string[] - } - ) { - return this.client.getBrands(filter) - /** - - Example of returned data: - - - - [ - - { - - "id": "brand_123", - - "name": "Brand 123", - - "product_id": "prod_321" - - }, - - { - - "id": "post_456", - - "name": "Brand 456", - - "product_id": "prod_654" - - } - - ] - */ - } -} -``` - -### Module Definition - -```ts -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" - -export const BRAND_MODULE = "brand" - -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) -``` - -Then, you can use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve a product and its brand from the third-party service in a single query. - -```ts title="Query Example" highlights={queryHighlights} -const { result } = await query.graph({ - entity: "product", - fields: ["id", "brand.*"], - filters: { - id: "prod_123", - }, -}) - -// result = [{ -// id: "prod_123", -// brand: { -// id: "brand_123", -// name: "Brand 123", -// product_id: "prod_123" -// } -// ... -// }] -``` - -Query simplifies the process of retrieving data across systems, as you can retrieve data from multiple sources in a single query. - -#### Examples - -The following tutorials are step-by-step guides that show you how to query data across systems using the Medusa Framework. - -- [Integrate Sanity CMS](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md): Query data from third-party services using read-only links. - -#### Start Learning - -To learn more about the different concepts useful for querying data across systems, check out the following chapters: - -- [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Read-Only Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) -- [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) - -*** - -## Automate Tasks - -The Medusa Framework provides the tools to automate tasks in your Medusa application. Automation is useful when you want to perform a task periodically, such as syncing data, or when an event occurs, such as sending a confirmation email when an order is placed. - -To build the task to be automated, you first create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that contains the task's logic, such as syncing data or sending an email. - -### Step - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { CreateNotificationDTO } from "@medusajs/framework/types" - -export const sendNotificationStep = createStep( - "send-notification", - async (data: CreateNotificationDTO[], { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) - const notification = await notificationModuleService.createNotifications( - data - ) - return new StepResponse(notification) - } -) -``` - -### Workflow - -```ts -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { sendNotificationStep } from "./steps/send-notification" - -type WorkflowInput = { - id: string -} - -export const sendOrderConfirmationWorkflow = createWorkflow( - "send-order-confirmation", - ({ id }: WorkflowInput) => { - // @ts-ignore - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "id", - "email", - "currency_code", - "total", - "items.*", - ], - filters: { - id, - }, - }) - - const notification = sendNotificationStep([{ - to: orders[0].email, - channel: "email", - template: "order-placed", - data: { - order: orders[0], - }, - }]) - - return new WorkflowResponse(notification) - } -) -``` - -Then, you can execute this workflow when an event occurs using a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), or at a specific interval using a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). - -### Event Subscriber - -```ts highlights={orderPlacedHandlerHighlights} -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" - -export default async function orderPlacedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await sendOrderConfirmationWorkflow(container) - .run({ - input: { - id: data.id, - }, - }) -} - -export const config: SubscriberConfig = { - event: "order.placed", -} -``` - -### Scheduled Job - -```ts highlights={orderConfirmationJobHighlights} -import type { - MedusaContainer, -} from "@medusajs/framework/types" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" - -export default async function orderConfirmationJob( - container: MedusaContainer -) { - await sendOrderConfirmationWorkflow(container).run({ - input: { - id: "order_123", - }, - }) -} -export const config = { - name: "order-confirmation-job", - schedule: "0 0 * * *", // Every day at midnight -} -``` - -### Examples - -The following guides are step-by-step guides that show you how to automate tasks using the Medusa Framework. - -- [Restock Notifications](https://docs.medusajs.com/resources/recipes/commerce-automation/restock-notification/index.html.md): Send restock notifications to customers when a product is back in stock. -- [Sync Data from and to ERP](https://docs.medusajs.com/resources/recipes/erp#sync-products-from-erp/index.html.md): Sync data between your Medusa application and an ERP system. - -### Start Learning - -To learn more about the different concepts useful for automating tasks, check out the following chapters: - -- [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Events and Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) - -*** - -## Re-Use Customizations Across Applications - -If you have custom features that you want to re-use across multiple Medusa applications, or you want to publish your customizations for the community to use, you can build a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -A plugin encapsulates your customizations in a single package. The customizations include [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), and more. - -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) - -You can then publish that plugin to NPM and install it in any Medusa application. This allows you to re-use your customizations efficiently across multiple projects, or share them with the community. - -### Examples - -The following tutorials are step-by-step guides that show you how to build plugins using the Medusa Framework. - -- [Wishlist Plugin](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md): Build a wishlist plugin for your Medusa application. -- [Migrate Data from Magento Plugin](https://docs.medusajs.com/resources/integrations/guides/magento/index.html.md): Build a plugin that migrates data from Magento to your Medusa application. - -### Start Learning - -To learn more about the different concepts useful for building plugins, check out the following chapters: - -- [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) - - -# Medusa Container - -In this chapter, you’ll learn about the Medusa container and how to use it. - -## What is the Medusa Container? - -The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. - -In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. - -Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. - -For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: - -```ts highlights={highlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - res.json({ - products, - }) -} -``` - -The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. - -You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -*** - -## List of Resources Registered in the Medusa Container - -Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) - -*** - -## How to Resolve From the Medusa Container - -This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. - -### Subscriber - -A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={subscriberHighlights} -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: data.id, - }, - }) - - console.log(`You created a product with the title ${products[0].title}`) -} - -export const config: SubscriberConfig = { - event: `product.created`, -} -``` - -### Scheduled Job - -A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={scheduledJobHighlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - console.log( - `You have ${products.length} matching your filters.` - ) -} - -export const config = { - name: "every-minute-message", - // execute every minute - schedule: "* * * * *", -} -``` - -### Workflow Step - -A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={workflowStepsHighlight} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -const step1 = createStep("step-1", async (_, { container }) => { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - return new StepResponse(products) -}) -``` - -### Module Services and Loaders - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. - -Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). - - -# Define Module Link - -In this chapter, you’ll learn what a module link is and how to define one. - -## What is a Module Link? - -Medusa's modular architecture isolates modules from one another to ensure they can be integrated into your application without side effects. Module isolation has other benefits, which you can learn about in the [Module Isolation chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md). Since modules are isolated, you can't access another module's data models to add a relation to it or extend it. Instead, you use a module link. - -A module link forms an association between two data models of different modules while maintaining module isolation. Using module links, you can build virtual relations between your custom data models and data models in the Commerce Modules, which is useful as you extend the features provided by the Commerce Modules. Then, Medusa creates a link table in the database to store the IDs of the linked records. You'll learn more about link tables later in this chapter. - -For example, the [Brand Customizations Tutorial](https://docs.medusajs.com/learn/customization/extend-features/index.html.md) shows how to create a Brand Module that adds the concept of brands to your application, then link those brands to a product. - -*** - -## How to Define a Module Link? - -### 1. Create Link File - -Module links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines the link using `defineLink` from the Modules SDK and exports it. - -For example: - -```ts title="src/links/blog-product.ts" highlights={highlights} -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - ProductModule.linkable.product, - BlogModule.linkable.post -) -``` - -The `defineLink` function accepts as parameters the link configurations of each module's data model. A module has a special `linkable` property that holds these configurations for its data models. - -In this example, you define a module link between the `blog` module's `post` data model and the Product Module's `Product` data model. - -### 2. Sync Links - -After defining the link, run the `db:sync-links` command: - -```bash -npx medusa db:sync-links -``` - -The Medusa application creates a new table for your module link to store the IDs of linked records. - -You can also use the `db:migrate` command, which runs both the migrations and syncs the links. - -Use either of these commands whenever you make changes to your link definitions. For example, run this command if you remove your link definition file. - -*** - -### Module Link's Database Table - -When you define a module link, the Medusa application creates a table in the database for that module link. The table's name is a combination of the names of the two data models linked in the format `module1_table1_module2_table2`, where: - -- `module1` and `module2` are the names of the modules. -- `table1` and `table2` are the table names of the data models. - -For example, if you define a link between the `Product` data model from the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) and a `Post` data model from a Blog Module, the table name would be `product_product_blog_post`. - -The table has two columns, each storing the ID of a record from the linked data models. For example, the `product_product_blog_post` table would have columns `product_id` and `post_id`. These columns store only the IDs of the linked records and do not hold a foreign key constraint. - -Then, when you create links between records of the data models, the IDs of these data models are stored as a new record in the link's table. - -You can also add custom columns in the link table as explained in the [Add Columns to Link Table chapter](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). - -![Diagram illustration for module links](https://res.cloudinary.com/dza7lstvk/image/upload/v1741696766/Medusa%20Book/custom-links_vezsx8.jpg) - -*** - -## When to Use Module Links - -- You want to create a relation between data models from different modules. -- You want to extend the data model of another module. - -You want to create a relationship between data models in the same module. Use data model relationships instead. - -*** - -## Define a List Module Link - -By default, a module link establishes a one-to-one relation: a record of a data model is linked to one record of the other data model. - -To specify that a data model can have multiple of its records linked to the other data model's record, use the `isList` option. - -For example: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - ProductModule.linkable.product, - { - linkable: BlogModule.linkable.post, - isList: true, - } -) -``` - -In this case, you pass an object of configuration as a parameter instead. The object accepts the following properties: - -- `linkable`: The data model's link configuration. -- `isList`: Whether multiple records can be linked to one record of the other data model. - -In this example, a record of `product` can be linked to more than one record of `post`. - -### Many-to-Many Module Link - -Your module link can also establish a many-to-many relation between the linked data models. To do this, enable `isList` on both sides of the link. - -For example: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - { - linkable: BlogModule.linkable.post, - isList: true, - } -) -``` - -*** - -## Set Delete Cascades on Link - -To enable delete cascade on a link so that when a record is deleted, its linked records are also deleted, pass the `deleteCascade` property in the object passed to `defineLink`. - -For example: - -```ts -import BlogModule from "../modules/blog" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - ProductModule.linkable.product, - { - linkable: BlogModule.linkable.post, - deleteCascade: true, - } -) -``` - -In this example, when a product is deleted, its linked `post` record is also deleted. - -*** - -## Renaming Participants in a Module Link - -As mentioned in the [Module Link's Database Table](#module-links-database-table) section, the name of a link's table consists of the names of the modules and the data models' table names. - -So, if you rename a module or a data model's table, then run the `db:sync-links` or `db:migrate` commands, you'll be asked to delete the old link table and create a new one. - -A data model's table name is passed in the first parameter of `model.define`, and a module's name is passed in the first parameter of `Module` in the module's `index.ts` file. - -For example, if you have the link table `product_product_blog_post` and you rename the Blog Module from `blog` to `article`, Medusa considers the old link definition deleted. Then, when you run the `db:sync-links` or `db:migrate` command, Medusa will ask if you want to delete the old link table, and will create a new one with the new name `product_product_article_post`. - -To resolve this, you can rename the link table in the link definition. - -### Rename Link Table - -If you need to rename a module or its data model's table, you can persist the old name by passing a third parameter to `defineLink`. This parameter is an object of additional configurations. It accepts a `database` property that allows you to configure the link's table name. - -For example, after renaming the Blog Module to `article`, you can persist the old name `blog` in the link table name: - -```ts highlights={renameHighlights} -import ArticleModule from "../modules/article" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - ProductModule.linkable.product, - { - linkable: ArticleModule.linkable.post, - isList: true, - }, - { - database: { - table: "product_product_blog_post", - }, - } -) -``` - -In this example, you set the `table` property in the `database` object to the old link table name `product_product_blog_post`, ensuring that the old link table is not deleted. - -This is enough to rename the link table when you rename a module. If you renamed a data model's table, you need to also run the `db:sync-links` or `db:migrate` commands, which will update the column names in the link table automatically: - -```bash -npx medusa db:migrate -``` - -*** - -## Delete Module Link Definition - -To delete a module link definition, remove the link file from the `src/links` directory. Then, run the `db:sync-links` or `db:migrate` command to delete the link table from the database: - -```bash -npx medusa db:migrate -``` - - -# Modules - -In this chapter, you’ll learn about modules and how to create them. - -## What is a Module? - -A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](https://docs.medusajs.com/resources/commerce-modules/cart/index.html.md) that holds the data models and business logic for cart operations. - -When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations. - -Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without affecting the existing setup. You can also re-use your modules across Medusa projects. - -As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem. - -- You want to build a custom feature related to a single domain or integrate a third-party service. - -- You want to create a reusable package of customizations that include not only modules, but also API routes, workflows, and other customizations. Instead, use a [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -*** - -## How to Create a Module? - -In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) so that you can build commerce flows and features around the functionalities provided by the module. - -In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model. You'll also expose an API endpoint to create a blog post. - -Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. - -### 1. Create Data Model - -A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. - -You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a `Post` data model in the Blog Module, create the file `src/modules/blog/models/post.ts` with the following content: - -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) - -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) - -export default Post -``` - -You define the data model using the `define` method of the DML. It accepts two parameters: - -1. The first one is the name of the data model's table in the database. Use snake-case names. -2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. - - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. - -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#property-types/index.html.md). - -The code snippet above defines a `Post` data model with `id` and `title` properties. - -### 2. Create Service - -You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), allowing you to resolve and use it when building custom commerce flows. - -You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content: - -![Updated directory overview after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732807230/Medusa%20Book/blog-dir-overview-2_avzb9l.jpg) - -```ts title="src/modules/blog/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ -} - -export default BlogModuleService -``` - -Your module's service extends a class generated by `MedusaService` from the Modules SDK. This class comes with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on each of your modules, saving you time that can be spent on building custom business logic. - -The `MedusaService` function accepts an object of data models to generate methods for. You can pass all data models in your module in this object. - -For example, the `BlogModuleService` now has a `createPosts` method to create post records, and a `retrievePost` method to retrieve a post record. The suffix of each method (except for `retrieve`) is the pluralized name of the data model. - -Find all methods generated by the `MedusaService` in [this reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) - -If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend `MedusaService`. - -### 3. Export Module Definition - -The final piece to a module is its definition, which is exported in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name. - -So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content: - -![Updated directory overview after adding the module definition](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808511/Medusa%20Book/blog-dir-overview-3_dcgjaa.jpg) - -```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights} -import BlogModuleService from "./service" -import { Module } from "@medusajs/framework/utils" - -export const BLOG_MODULE = "blog" - -export default Module(BLOG_MODULE, { - service: BlogModuleService, -}) -``` - -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: - -1. The name that the module's main service is registered under (`blog`). The module name can contain only alphanumeric characters and underscores. -2. An object with a required property `service` indicating the module's main service. - -You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations. - -### 4. Add Module to Medusa's Configurations - -If you're creating the module in a plugin, this step isn't required as the module is registered when the plugin is registered. Learn more about plugins in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations. - -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: - -```ts title="medusa-config.ts" highlights={[["7"]]} -module.exports = defineConfig({ - projectConfig: { - // ... - }, - modules: [ - { - resolve: "./src/modules/blog", - }, - ], -}) -``` - -Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. - -### 5. Generate Migrations - -Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module. - -Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change. - -To generate a migration for the Blog Module, run the following command in your Medusa application's directory: - -If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. - -```bash -npx medusa db:generate blog -``` - -The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: - -```ts -import { Migration } from "@mikro-orm/migrations" - -export class Migration20241121103722 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"post\" cascade;") - } - -} -``` - -In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. - -### 6. Run Migrations - -To reflect the changes in the generated migration file on the database, run the `db:migrate` command: - -If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. - -```bash -npx medusa db:migrate -``` - -This creates the `post` table in the database. - -*** - -## Test the Module - -Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods. - -To test out the Blog Module, you'll add the functionality to create a post in a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that creates a blog post by executing the workflow. - -By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -To create the workflow, create the file `src/workflows/create-post.ts` with the following content: - -```ts title="src/workflows/create-post.ts" highlights={workflowHighlights} -import { - createStep, - createWorkflow, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { BLOG_MODULE } from "../modules/blog" -import BlogModuleService from "../modules/blog/service" - -type CreatePostWorkflowInput = { - title: string -} - -const createPostStep = createStep( - "create-post", - async ({ title }: CreatePostWorkflowInput, { container }) => { - const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) - - const post = await blogModuleService.createPosts({ - title, - }) - - return new StepResponse(post, post) - }, - async (post, { container }) => { - const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE) - - await blogModuleService.deletePosts(post.id) - } -) - -export const createPostWorkflow = createWorkflow( - "create-post", - (postInput: CreatePostWorkflowInput) => { - const post = createPostStep(postInput) - - return new WorkflowResponse(post) - } -) -``` - -The workflow has a single step `createPostStep` that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method `createPosts` of the service, which was generated by `MedusaService`. - -The step also has a compensation function, which is a function passed as a third-parameter to `createStep` that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution. - -You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file `src/api/blog/posts/route.ts` with the following content: - -```ts -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createPostWorkflow, -} from "../../../workflows/create-post" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const { result: post } = await createPostWorkflow(req.scope) - .run({ - input: { - title: "My Post", - }, - }) - - res.json({ - post, - }) -} -``` - -This adds a `POST` API route at `/blog/posts`. In the API route, you execute the `createPostWorkflow` by invoking it, passing it the Medusa container in `req.scope`, then invoking the `run` method. In the `run` method, you pass the workflow's input in the `input` property. - -To test this out, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `POST` request to `/blog/posts`: - -```bash -curl -X POST http://localhost:9000/blog/posts -``` - -This will create a post and return it in the response: - -```json -{ - "post": { - "id": "123...", - "title": "My Post", - "created_at": "...", - "updated_at": "..." - } -} -``` - -You can also execute the workflow from a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) when an event occurs, or from a [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) to run it at a specified interval. +![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) # Scheduled Jobs @@ -4192,371 +4557,6 @@ In the scheduled job function, you execute the `syncProductToErpWorkflow` by inv The next time you start the Medusa application, it will run this job every day at midnight. -# Plugins - -In this chapter, you'll learn what a plugin is in Medusa. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## What is a Plugin? - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). - -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. - -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) - -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). - -*** - -## Plugin vs Module - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. - -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. - -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. - -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. - -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. - -*** - -## How to Create a Plugin? - -The next chapter explains how you can create and publish a plugin. - - -# Medusa's Architecture - -In this chapter, you'll learn about the architectural layers in Medusa. - -Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). - -## HTTP, Workflow, and Module Layers - -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. - -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: - -1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. - -These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) - -*** - -## Database Layer - -The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) - -*** - -## Third-Party Integrations Layer - -Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -### Commerce Modules - -[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. - -You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. - -You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. - -![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) - -### Infrastructure Modules - -[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: - -- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). -- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. -- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. -- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. -- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. -- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. - -All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. - -![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) - -*** - -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture including all its layers. - -![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) - - -# Workflows - -In this chapter, you’ll learn about workflows and how to define and execute them. - -## What is a Workflow? - -In digital commerce you typically have many systems involved in your operations. For example, you may have an ERP system that holds product master data and accounting reports, a CMS system for content, a CRM system for managing customer campaigns, a payment service to process credit cards, and so on. Sometimes you may even have custom built applications that need to participate in the commerce stack. One of the biggest challenges when operating a stack like this is ensuring consistency in the data spread across systems. - -Medusa has a built-in durable execution engine to help complete tasks that span multiple systems. You orchestrate your operations across systems in Medusa instead of having to manage it yourself. Other commerce platforms don't have this capability, which makes them a bottleneck to building customizations and iterating quickly. - -A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to how you create a JavaScript function. - -However, unlike regular functions, workflows: - -- Create an internal representation of your steps, allowing you to track them and their progress. -- Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems. -- Perform long actions asynchronously, giving you control over when a step starts and finishes. - -You implement all custom flows within workflows, then execute them from [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), and [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). - -*** - -## How to Create and Execute a Workflow? - -### 1. Create the Steps - -A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK. - -Create the file `src/workflows/hello-world.ts` with the following content: - -![Example of workflow file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866980/Medusa%20Book/workflow-dir-overview_xklukj.jpg) - -```ts title="src/workflows/hello-world.ts" highlights={step1Highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - return new StepResponse(`Hello from step one!`) - } -) -``` - -The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter. - -Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step. - -Steps can accept input parameters. For example, add the following to `src/workflows/hello-world.ts`: - -```ts title="src/workflows/hello-world.ts" highlights={step2Highlights} -type WorkflowInput = { - name: string -} - -const step2 = createStep( - "step-2", - async ({ name }: WorkflowInput) => { - return new StepResponse(`Hello ${name} from step two!`) - } -) -``` - -This adds another step whose function accepts as a parameter an object with a `name` property. - -### 2. Create a Workflow - -Next, add the following to the same file to create the workflow using the `createWorkflow` function: - -```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights} -import { - // other imports... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1() - // to pass input - const str2 = step2(input) - - return new WorkflowResponse({ - message: str2, - }) - } -) - -export default myWorkflow -``` - -The `createWorkflow` function accepts the workflow's unique name as a first parameter, and the workflow's function as a second parameter. The workflow can accept input which is passed as a parameter to the function. - -The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors. - -### 3. Execute the Workflow - -You can execute a workflow from different customizations: - -- Execute in an API route to expose the workflow's functionalities to clients. -- Execute in a subscriber to use the workflow's functionalities when a commerce operation is performed. -- Execute in a scheduled job to run the workflow's functionalities automatically at a specified repeated interval. - -To execute the workflow, invoke it passing the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. Then, use its `run` method: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../workflows/hello-world" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run({ - input: { - name: "John", - }, - }) - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/order-placed.ts" highlights={[["11"], ["12"], ["13"], ["14"], ["15"], ["16"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import myWorkflow from "../workflows/hello-world" - -export default async function handleOrderPlaced({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await myWorkflow(container) - .run({ - input: { - name: "John", - }, - }) - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "order.placed", -} -``` - -### Scheduled Job - -```ts title="src/jobs/message-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"], ["11"], ["12"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import myWorkflow from "../workflows/hello-world" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await myWorkflow(container) - .run({ - input: { - name: "John", - }, - }) - - console.log(result.message) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -}; -``` - -### 4. Test Workflow - -To test out your workflow, start your Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, if you added the API route above, send a `GET` request to `/workflow`: - -```bash -curl http://localhost:9000/workflow -``` - -You’ll receive the following response: - -```json title="Example Response" -{ - "message": "Hello John from step two!" -} -``` - -*** - -## Access Medusa Container in Workflow Steps - -A step receives an object as a second parameter with configurations and context-related properties. One of these properties is the `container` property, which is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to allow you to resolve Framework and commerce tools in your application. - -For example, consider you want to implement a workflow that returns the total products in your application. Create the file `src/workflows/product-count.ts` with the following content: - -```ts title="src/workflows/product-count.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const getProductCountStep = createStep( - "get-product-count", - async (_, { container }) => { - const productModuleService = container.resolve("product") - - const [, count] = await productModuleService.listAndCountProducts() - - return new StepResponse(count) - } -) - -const productCountWorkflow = createWorkflow( - "product-count", - function () { - const count = getProductCountStep() - - return new WorkflowResponse({ - count, - }) - } -) - -export default productCountWorkflow -``` - -In `getProductCountStep`, you use the `container` to resolve the Product Module's main service. Then, you use its `listAndCountProducts` method to retrieve the total count of products and return it in the step's response. You then execute this step in the `productCountWorkflow`. - -You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products. - -Find a full list of the registered resources in the Medusa container and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). You can use these resources in your custom workflows. - - # Worker Mode of Medusa Instance In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. @@ -5111,6 +5111,378 @@ Your customizations often span across systems, where you need to retrieve data o In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. +# Guide: Add Product's Brand Widget in Admin + +In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. + +### Prerequisites + +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +## 1. Initialize JS SDK + +In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. + +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: + +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +You initialize the SDK passing it the following options: + +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. + +Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +You can now use the SDK to send requests to the Medusa server. + +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). + +*** + +## 2. Add Widget to Product Details Page + +You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. + +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). + +To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: + +![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) + +```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" +import { clx, Container, Heading, Text } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" + +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string + } +} + +const ProductBrandWidget = ({ + data: product, +}: DetailWidgetProps) => { + const { data: queryResult } = useQuery({ + queryFn: () => sdk.admin.product.retrieve(product.id, { + fields: "+brand.*", + }), + queryKey: [["product", product.id]], + }) + const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name + + return ( + +
+
+ Brand +
+
+
+ + Name + + + + {brandName || "-"} + +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductBrandWidget +``` + +A widget's file must export: + +- A React component to be rendered in the specified injection zone. The component must be the file's default export. +- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. + +Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. + +In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. + +*** + +## Test it Out + +To test out your widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. + +![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) + +*** + +## Admin Components Guides + +When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. + +The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. + +*** + +## Next Chapter: Add UI Route for Brands + +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. + + +# Guide: Define Module Link Between Brand and Product + +In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. + +Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. + +A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. + +In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. + +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +### Prerequisites + +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +## 1. Define Link + +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. + +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: + +![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) + +```ts title="src/links/product-brand.ts" highlights={highlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` + +You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. + +The `defineLink` function accepts two parameters of the same type, which is either: + +- The data model's link configuration, which you access from the Module's `linkable` property; +- Or an object that has two properties: + - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. + - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. + +So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. + +*** + +## 2. Sync the Link to the Database + +A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: + +```bash +npx medusa db:migrate +``` + +This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. + +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. + +*** + +## Next Steps: Extend Create Product Flow + +In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. + + +# Guide: Query Product's Brands + +In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. + +In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +*** + +## Approach 1: Retrieve Brands in Existing API Routes + +Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. + +Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. + +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +Any product that is linked to a brand will have a `brand` property in its object: + +```json title="Example Product Object" +{ + "id": "prod_123", + // ... + "brand": { + "id": "01JEB44M61BRM3ARM2RRMK7GJF", + "name": "Acme", + "created_at": "2024-12-05T09:59:08.737Z", + "updated_at": "2024-12-05T09:59:08.737Z", + "deleted_at": null + } +} +``` + +By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. + +### Limitations: Filtering by Brands in Existing API Routes + +While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. + +Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Approach 2: Use Query to Retrieve Linked Records + +You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. + +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: + +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) + + res.json({ brands }) +} +``` + +This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: + +- `entity`: The data model's name as specified in the first parameter of `model.define`. +- `fields`: An array of properties and relations to retrieve. You can pass: + - A property's name, such as `id`, or `*` for all properties. + - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. + +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. + +### Test it Out + +To test the API route out, send a `GET` request to `/admin/brands`: + +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +This returns the brands in your store with their linked products. For example: + +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` + +### Limitations: Filtering by Brand in Query + +While you can use Query to retrieve linked records, you can't filter by linked records. + +For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Summary + +By following the examples of the previous chapters, you: + +- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. +- Extended the create-product workflow and route to allow setting the product's brand while creating the product. +- Queried a product's brand, and vice versa. + +*** + +## Next Steps: Customize Medusa Admin + +Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. + +In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. + + # Guide: Create Brand API Route In the previous two chapters, you created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that added the concepts of brands to your application, then created a [workflow to create a brand](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. @@ -5319,158 +5691,216 @@ Now that you have brands in your Medusa application, you want to associate a bra In the next chapters, you'll learn how to build associations between data models defined in different modules. -# Guide: Add Product's Brand Widget in Admin +# Guide: Extend Create Product Flow -In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. +After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. + +Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. + +So, in this chapter, to extend the create product flow and associate a brand with a product, you will: + +- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. +- Extend the Create Product API route to allow passing a brand ID in `additional_data`. + +To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). ### Prerequisites -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -## 1. Initialize JS SDK - -In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. - -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: - -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) - -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -You initialize the SDK passing it the following options: - -- `baseUrl`: The URL to the Medusa server. -- `debug`: Whether to enable logging debug messages. This should only be enabled in development. -- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. - -Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -You can now use the SDK to send requests to the Medusa server. - -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) *** -## 2. Add Widget to Product Details Page +## 1. Consume the productsCreated Hook -You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. +A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: +The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters. -![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: -```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" -import { clx, Container, Heading, Text } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/sdk" +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string - } -} +```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" +import { BRAND_MODULE } from "../../modules/brand" +import BrandModuleService from "../../modules/brand/service" -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps) => { - const { data: queryResult } = useQuery({ - queryFn: () => sdk.admin.product.retrieve(product.id, { - fields: "+brand.*", - }), - queryKey: [["product", product.id]], +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand_id) { + return new StepResponse([], []) + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + // if the brand doesn't exist, an error is thrown. + await brandModuleService.retrieveBrand(additional_data.brand_id as string) + + // TODO link brand to product }) - const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - - return ( - -
-
- Brand -
-
-
- - Name - - - - {brandName || "-"} - -
-
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductBrandWidget +) ``` -A widget's file must export: +Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: -- A React component to be rendered in the specified injection zone. The component must be the file's default export. -- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. +1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. +2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools. -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. +In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. -In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. +### Link Brand to Product -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. +Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. -You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. +Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). + +To use Link in the `productsCreated` hook, replace the `TODO` with the following: + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} +const link = container.resolve("link") +const logger = container.resolve("logger") + +const links: LinkDefinition[] = [] + +for (const product of products) { + links.push({ + [Modules.PRODUCT]: { + product_id: product.id, + }, + [BRAND_MODULE]: { + brand_id: additional_data.brand_id, + }, + }) +} + +await link.create(links) + +logger.info("Linked brand to products") + +return new StepResponse(links, links) +``` + +You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. + +Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. + +![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) + +Finally, you return an instance of `StepResponse` returning the created links. + +### Dismiss Links in Compensation + +You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. + +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: + +```ts title="src/workflows/hooks/created-product.ts" +createProductsWorkflow.hooks.productsCreated( + // ... + (async (links, { container }) => { + if (!links?.length) { + return + } + + const link = container.resolve("link") + + await link.dismiss(links) + }) +) +``` + +In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. + +*** + +## 2. Configure Additional Data Validation + +Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. + +You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: + +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +// ... + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) +``` + +Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). + +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. *** ## Test it Out -To test out your widget, start the Medusa application: +To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: -```bash npm2yarn -npm run dev +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' ``` -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. +Make sure to replace the email and password in the request body with your user's credentials. -![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) +Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "options": [ + { + "title": "Default option", + "values": ["Default option value"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand_id": "{brand_id}" + } +}' +``` + +Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). + +The request creates a product and returns it. + +In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products. *** -## Admin Components Guides +## Next Steps: Query Linked Brands and Products -When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. - -The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. - -*** - -## Next Chapter: Add UI Route for Brands - -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. +Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. # Guide: Implement Brand Module @@ -5769,708 +6199,6 @@ You now have a `createBrandWorkflow` that you can execute to create a brand. In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. -# Guide: Extend Create Product Flow - -After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. - -Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. - -So, in this chapter, to extend the create product flow and associate a brand with a product, you will: - -- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. -- Extend the Create Product API route to allow passing a brand ID in `additional_data`. - -To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -*** - -## 1. Consume the productsCreated Hook - -A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. - -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - -The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters. - -To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: - -![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) - -```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { LinkDefinition } from "@medusajs/framework/types" -import { BRAND_MODULE } from "../../modules/brand" -import BrandModuleService from "../../modules/brand/service" - -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand_id) { - return new StepResponse([], []) - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - // if the brand doesn't exist, an error is thrown. - await brandModuleService.retrieveBrand(additional_data.brand_id as string) - - // TODO link brand to product - }) -) -``` - -Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: - -1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. -2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools. - -In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. - -### Link Brand to Product - -Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. - -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). - -To use Link in the `productsCreated` hook, replace the `TODO` with the following: - -```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} -const link = container.resolve("link") -const logger = container.resolve("logger") - -const links: LinkDefinition[] = [] - -for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) -} - -await link.create(links) - -logger.info("Linked brand to products") - -return new StepResponse(links, links) -``` - -You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. - -Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. - -![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) - -Finally, you return an instance of `StepResponse` returning the created links. - -### Dismiss Links in Compensation - -You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. - -To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: - -```ts title="src/workflows/hooks/created-product.ts" -createProductsWorkflow.hooks.productsCreated( - // ... - (async (links, { container }) => { - if (!links?.length) { - return - } - - const link = container.resolve("link") - - await link.dismiss(links) - }) -) -``` - -In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. - -*** - -## 2. Configure Additional Data Validation - -Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. - -You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: - -![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) - -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" - -// ... - -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) -``` - -Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). - -So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. - -*** - -## Test it Out - -To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password in the request body with your user's credentials. - -Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: - -```bash -curl -X POST 'http://localhost:9000/admin/products' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "title": "Product 1", - "options": [ - { - "title": "Default option", - "values": ["Default option value"] - } - ], - "shipping_profile_id": "{shipping_profile_id}", - "additional_data": { - "brand_id": "{brand_id}" - } -}' -``` - -Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). - -The request creates a product and returns it. - -In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products. - -*** - -## Next Steps: Query Linked Brands and Products - -Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. - - -# Guide: Define Module Link Between Brand and Product - -In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. - -Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. - -A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. - -In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. - -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -### Prerequisites - -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -## 1. Define Link - -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. - -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: - -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - -```ts title="src/links/product-brand.ts" highlights={highlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. - -The `defineLink` function accepts two parameters of the same type, which is either: - -- The data model's link configuration, which you access from the Module's `linkable` property; -- Or an object that has two properties: - - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. - - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. - -So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. - -*** - -## 2. Sync the Link to the Database - -A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: - -```bash -npx medusa db:migrate -``` - -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. - -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. - -*** - -## Next Steps: Extend Create Product Flow - -In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. - - -# Guide: Query Product's Brands - -In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. - -In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -*** - -## Approach 1: Retrieve Brands in Existing API Routes - -Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. - -Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. - -For example, send the following request to retrieve the list of products with their brands: - -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -Any product that is linked to a brand will have a `brand` property in its object: - -```json title="Example Product Object" -{ - "id": "prod_123", - // ... - "brand": { - "id": "01JEB44M61BRM3ARM2RRMK7GJF", - "name": "Acme", - "created_at": "2024-12-05T09:59:08.737Z", - "updated_at": "2024-12-05T09:59:08.737Z", - "deleted_at": null - } -} -``` - -By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. - -### Limitations: Filtering by Brands in Existing API Routes - -While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. - -Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Approach 2: Use Query to Retrieve Linked Records - -You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. - -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: - -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) - - res.json({ brands }) -} -``` - -This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: - -- `entity`: The data model's name as specified in the first parameter of `model.define`. -- `fields`: An array of properties and relations to retrieve. You can pass: - - A property's name, such as `id`, or `*` for all properties. - - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. - -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. - -### Test it Out - -To test the API route out, send a `GET` request to `/admin/brands`: - -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -This returns the brands in your store with their linked products. For example: - -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` - -### Limitations: Filtering by Brand in Query - -While you can use Query to retrieve linked records, you can't filter by linked records. - -For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Summary - -By following the examples of the previous chapters, you: - -- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. -- Extended the create-product workflow and route to allow setting the product's brand while creating the product. -- Queried a product's brand, and vice versa. - -*** - -## Next Steps: Customize Medusa Admin - -Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. - -In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. - - -# Guide: Sync Brands from Medusa to Third-Party - -In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. - -In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. - -Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. - -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. - -### Prerequisites - -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) - -## 1. Emit Event in createBrandWorkflow - -Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. - -Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: - -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -// ... - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... - - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) - - return new WorkflowResponse(brand) - } -) -``` - -The `emitEventStep` accepts an object parameter having two properties: - -- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. -- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. - -You'll learn how to handle this event in a later step. - -*** - -## 2. Create Sync to Third-Party System Workflow - -The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -You'll create a `syncBrandToSystemWorkflow` that has two steps: - -- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. -- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. - -### syncBrandToCmsStep - -To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Brand } from "../modules/brand/models/brand" -import { CMS_MODULE } from "../modules/cms" -import CmsModuleService from "../modules/cms/service" - -type SyncBrandToCmsStepInput = { - brand: InferTypeOf -} - -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.createBrand(brand) - - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } - - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.deleteBrand(id) - } -) -``` - -You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. - -You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Create Workflow - -You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -type SyncBrandToCmsWorkflowInput = { - id: string -} - -export const syncBrandToCmsWorkflow = createWorkflow( - "sync-brand-to-cms", - (input: SyncBrandToCmsWorkflowInput) => { - // @ts-ignore - const { data: brands } = useQueryGraphStep({ - entity: "brand", - fields: ["*"], - filters: { - id: input.id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) -``` - -You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: - -- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. -- `syncBrandToCmsStep`: Create the brand in the third-party CMS. - -You'll execute this workflow in the subscriber next. - -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). - -*** - -## 3. Handle brand.created Event - -You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. - -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: - -![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) - -```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} -import type { - SubscriberConfig, - SubscriberArgs, -} from "@medusajs/framework" -import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" - -export default async function brandCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncBrandToCmsWorkflow(container).run({ - input: data, - }) -} - -export const config: SubscriberConfig = { - event: "brand.created", -} -``` - -A subscriber file must export: - -- The asynchronous function that's executed when the event is emitted. This must be the file's default export. -- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. - -The subscriber function accepts an object parameter that has two properties: - -- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. -- `container`: The Medusa container used to resolve Framework and commerce tools. - -In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. - -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Test it Out - -To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. - -First, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: - -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` - -This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: - -```plain -info: Processing brand.created which has 1 subscribers -http: POST /admin/brands ← - (200) - 16.418 ms -info: Sending a POST request to /brands. -info: Request Data: { - "id": "01JEDWENYD361P664WRQPMC3J8", - "name": "Acme", - "created_at": "2024-12-06T11:42:32.909Z", - "updated_at": "2024-12-06T11:42:32.909Z", - "deleted_at": null -} -info: API Key: "123" -``` - -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa - -You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. - - # Guide: Schedule Syncing Brands from Third-Party In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. @@ -7021,81 +6749,395 @@ To manage that database, such as changing its name or perform operations on it i The next chapters provide examples of writing integration tests for API routes and workflows. -# Environment Variables in Admin Customizations +# Guide: Sync Brands from Medusa to Third-Party -In this chapter, you'll learn how to use environment variables in your admin customizations. +In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. -To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). +In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. -## How to Set Environment Variables +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. -The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -For example: +In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. -```plain -VITE_MY_API_KEY=sk_123 +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +## 1. Emit Event in createBrandWorkflow + +Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. + +Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: + +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +// ... + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... + + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) + + return new WorkflowResponse(brand) + } +) ``` +The `emitEventStep` accepts an object parameter having two properties: + +- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. +- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. + +You'll learn how to handle this event in a later step. + *** -## How to Use Environment Variables +## 2. Create Sync to Third-Party System Workflow -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. +The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. -For example: +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. -```tsx highlights={[["8"]]} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -const ProductWidget = () => { - return ( - -
- API Key: {import.meta.env.VITE_MY_API_KEY} -
-
- ) +You'll create a `syncBrandToSystemWorkflow` that has two steps: + +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. +- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. + +### syncBrandToCmsStep + +To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" +import { CMS_MODULE } from "../modules/cms" +import CmsModuleService from "../modules/cms/service" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf } -export const config = defineWidgetConfig({ - zone: "product.details.before", +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.createBrand(brand) + + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.deleteBrand(id) + } +) +``` + +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. + +You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. + +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +### Create Workflow + +You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +type SyncBrandToCmsWorkflowInput = { + id: string +} + +export const syncBrandToCmsWorkflow = createWorkflow( + "sync-brand-to-cms", + (input: SyncBrandToCmsWorkflowInput) => { + // @ts-ignore + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: ["*"], + filters: { + id: input.id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: + +- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. +- `syncBrandToCmsStep`: Create the brand in the third-party CMS. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: + +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) + +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} +import type { + SubscriberConfig, + SubscriberArgs, +} from "@medusajs/framework" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" + +export default async function brandCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await syncBrandToCmsWorkflow(container).run({ + input: data, + }) +} + +export const config: SubscriberConfig = { + event: "brand.created", +} +``` + +A subscriber file must export: + +- The asynchronous function that's executed when the event is emitted. This must be the file's default export. +- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. + +The subscriber function accepts an object parameter that has two properties: + +- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. +- `container`: The Medusa container used to resolve Framework and commerce tools. + +In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. + +First, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: + +```plain +info: Processing brand.created which has 1 subscribers +http: POST /admin/brands ← - (200) - 16.418 ms +info: Sending a POST request to /brands. +info: Request Data: { + "id": "01JEDWENYD361P664WRQPMC3J8", + "name": "Acme", + "created_at": "2024-12-06T11:42:32.909Z", + "updated_at": "2024-12-06T11:42:32.909Z", + "deleted_at": null +} +info: API Key: "123" +``` + +*** + +## Next Chapter: Sync Brand from Third-Party CMS to Medusa + +You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. + + +# Write Tests for Modules + +In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## moduleIntegrationTestRunner Utility + +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. + +For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: + +```ts title="src/modules/blog/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import Post from "../models/post" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [Post], + resolve: "./src/modules/blog", + testSuite: ({ service }) => { + // TODO write tests + }, }) -export default ProductWidget +jest.setTimeout(60 * 1000) ``` -In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: -### Type Error on import.meta.env +- `moduleName`: The name of the module. +- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. +- `resolve`: The path to the module's directory. +- `testSuite`: A function that defines the tests to run. -If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. -```ts title="src/admin/vite-env.d.ts" -/// -``` +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). *** -## Check Node Environment in Admin Customizations +## Run Tests -To check the current environment, Vite exposes two variables: +Run the following command to run your module integration tests: -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. +```bash npm2yarn +npm run test:integration:modules +``` -Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. *** -## Environment Variables in Production +## Pass Module Options -When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application. +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. -For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" + +moduleIntegrationTestRunner({ + moduleOptions: { + apiKey: "123", + }, + // ... +}) +``` + +*** + +## Write Tests for Modules without Data Models + +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. + +For example: + +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import BlogModuleService from "../service" +import { model } from "@medusajs/framework/utils" + +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) + +moduleIntegrationTestRunner({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` + +*** + +### Other Options and Inputs + +Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. + +*** + +## Database Used in Tests + +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. + +To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). # Admin Development Constraints @@ -7296,6 +7338,202 @@ export const handle = { Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. +# Environment Variables in Admin Customizations + +In this chapter, you'll learn how to use environment variables in your admin customizations. + +To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). + +## How to Set Environment Variables + +The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. + +For example: + +```plain +VITE_MY_API_KEY=sk_123 +``` + +*** + +## How to Use Environment Variables + +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. + +For example: + +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const ProductWidget = () => { + return ( + +
+ API Key: {import.meta.env.VITE_MY_API_KEY} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. + +### Type Error on import.meta.env + +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: + +```ts title="src/admin/vite-env.d.ts" +/// +``` + +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. + +*** + +## Check Node Environment in Admin Customizations + +To check the current environment, Vite exposes two variables: + +- `import.meta.env.DEV`: Returns `true` if the current environment is development. +- `import.meta.env.PROD`: Returns `true` if the current environment is production. + +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). + +*** + +## Environment Variables in Production + +When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application. + +For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. + + +# Admin Widgets + +In this chapter, you’ll learn more about widgets and how to use them. + +## What is an Admin Widget? + +The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. + +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. + +*** + +## How to Create a Widget? + +### Prerequisites + +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) + +You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. + +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: + +![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +// The widget +const ProductWidget = () => { + return ( + +
+ Product Widget +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. + +To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. + +In the example above, the widget is injected at the top of a product’s details. + +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open a product’s details page. You’ll find your custom widget at the top of the page. + +*** + +## Props Passed in Detail Pages + +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. + +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. + +*** + +## Injection Zone + +Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. + +*** + +## Admin Components List + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. + + # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -7532,256 +7770,6 @@ To build admin customizations that match the Medusa Admin's designs and layouts, For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -# Admin Development Tips - -In this chapter, you'll find some tips for your admin development. - -## Send Requests to API Routes - -To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: - -```ts -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). - -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. - -For example: - -### Query - -```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" - -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) - -export default ProductWidget -``` - -### Mutation - -```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useMutation } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" - -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) - - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - - - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). - -### Use Route Loaders for Initial Data - -You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). - -*** - -## Global Variables in Admin Customizations - -In your admin customizations, you can use the following global variables: - -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. -- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. -- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. - -*** - -## Admin Translations - -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. - -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). - - -# Admin Widgets - -In this chapter, you’ll learn more about widgets and how to use them. - -## What is an Admin Widget? - -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. - -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. - -*** - -## How to Create a Widget? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. - -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - -![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. - -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. - -In the example above, the widget is injected at the top of a product’s details. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page. You’ll find your custom widget at the top of the page. - -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" -import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" - -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps) => { - return ( - -
- - Product Widget {data.title} - -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. - -*** - -## Injection Zone - -Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. - -*** - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - # Pass Additional Data to Medusa's API Route In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. @@ -7981,55 +7969,156 @@ createProductsWorkflow.hooks.productsCreated( This updates the products to their original state before adding the brand to their `metadata` property. -# Handling CORS in API Routes +# Admin Development Tips -In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. +In this chapter, you'll find some tips for your admin development. -## CORS Overview +## Send Requests to API Routes -Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. +To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. -For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error. +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. -### CORS Configurations +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: -The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. +```ts +import Medusa from "@medusajs/js-sdk" -These configurations accept a URL pattern to identify allowed origins. - -For example: - -```js title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - storeCors: "http://localhost:8000", - adminCors: "http://localhost:7001", - // ... - }, +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", }, }) ``` -This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. +Notice that you use `import.meta.env` to access environment variables in your customizations, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md). +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). -*** - -## CORS in Store and Admin Routes - -To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. +Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. For example: -```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} -import type { - MedusaRequest, +### Query + +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + + {isLoading && Loading...} + {data?.products && ( +
    + {data.products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) + +export default ProductWidget +``` + +### Mutation + +```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) + + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + + + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). + +### Use Route Loaders for Initial Data + +You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). + +*** + +## Global Variables in Admin Customizations + +In your admin customizations, you can use the following global variables: + +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. +- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. +- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. + +*** + +## Admin Translations + +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. + +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). + + +# HTTP Methods + +In this chapter, you'll learn about how to add new API routes for each HTTP method. + +## HTTP Method Handler + +An API route is created for every HTTP method you export a handler function for in a route file. + +Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. + +For example, create the file `src/api/hello-world/route.ts` with the following content: + +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -export const GET = ( +export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { @@ -8038,59 +8127,20 @@ export const GET = ( }) } -export const CORS = false +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[POST] Hello world!", + }) +} ``` -This disables the CORS middleware on API Routes at the path `/store/custom`. +This adds two API Routes: -*** - -## CORS in Custom Routes - -If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error. - -You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. - -For example: - -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { defineMiddlewares } from "@medusajs/framework/http" -import type { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { ConfigModule } from "@medusajs/framework/types" -import { parseCorsOrigins } from "@medusajs/framework/utils" -import cors from "cors" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - const configModule: ConfigModule = - req.scope.resolve("configModule") - - return cors({ - origin: parseCorsOrigins( - configModule.projectConfig.http.storeCors - ), - credentials: true, - })(req, res, next) - }, - ], - }, - ], -}) -``` - -This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. # Throwing and Handling Errors @@ -8548,49 +8598,6 @@ A middleware can not override an existing middleware. Instead, middlewares are a For example, if you define a custom validation middleware, such as `validateAndTransformBody`, on an existing route, then both the original and the custom validation middleware will run. -# HTTP Methods - -In this chapter, you'll learn about how to add new API routes for each HTTP method. - -## HTTP Method Handler - -An API route is created for every HTTP method you export a handler function for in a route file. - -Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. - -For example, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[POST] Hello world!", - }) -} -``` - -This adds two API Routes: - -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. - - # API Route Parameters In this chapter, you’ll learn about path, query, and request body parameters. @@ -9211,123 +9218,116 @@ export const GET = async ( In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. -# Write Tests for Modules +# Handling CORS in API Routes -In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. +In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. -### Prerequisites +## CORS Overview -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. -## moduleIntegrationTestRunner Utility +For example, if you allow only origins starting with `http://localhost:7001` to access your Admin API Routes, other origins accessing those routes get a CORS error. -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. +### CORS Configurations -For example, assuming you have a `blog` module, create a test file at `src/modules/blog/__tests__/service.spec.ts`: +The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import Post from "../models/post" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [Post], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - // TODO write tests - }, -}) - -jest.setTimeout(60 * 1000) -``` - -The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: - -- `moduleName`: The name of the module. -- `moduleModels`: An array of models in the module. Refer to [this section](#write-tests-for-modules-without-data-models) if your module doesn't have data models. -- `resolve`: The path to the module's directory. -- `testSuite`: A function that defines the tests to run. - -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. - -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. - -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - -*** - -## Run Tests - -Run the following command to run your module integration tests: - -```bash npm2yarn -npm run test:integration:modules -``` - -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. - -*** - -## Pass Module Options - -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. +These configurations accept a URL pattern to identify allowed origins. For example: -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" - -moduleIntegrationTestRunner({ - moduleOptions: { - apiKey: "123", +```js title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + storeCors: "http://localhost:8000", + adminCors: "http://localhost:7001", + // ... + }, }, - // ... }) ``` +This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. + +Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/learn/configurations/medusa-config#http/index.html.md). + *** -## Write Tests for Modules without Data Models +## CORS in Store and Admin Routes -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. +To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. For example: -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import BlogModuleService from "../service" -import { model } from "@medusajs/framework/utils" +```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} -moduleIntegrationTestRunner({ - moduleModels: [DummyModel], - // ... -}) - -jest.setTimeout(60 * 1000) +export const CORS = false ``` -*** - -### Other Options and Inputs - -Refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +This disables the CORS middleware on API Routes at the path `/store/custom`. *** -## Database Used in Tests +## CORS in Custom Routes -The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. +If you create a route that doesn’t start with `/store` or `/admin`, you must apply the CORS middleware manually. Otherwise, all requests to your API route lead to a CORS error. -To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). +You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { defineMiddlewares } from "@medusajs/framework/http" +import type { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { ConfigModule } from "@medusajs/framework/types" +import { parseCorsOrigins } from "@medusajs/framework/utils" +import cors from "cors" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + middlewares: [ + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + const configModule: ConfigModule = + req.scope.resolve("configModule") + + return cors({ + origin: parseCorsOrigins( + configModule.projectConfig.http.storeCors + ), + credentials: true, + })(req, res, next) + }, + ], + }, + ], +}) +``` + +This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. # Retrieve Custom Links from Medusa's API Route @@ -9661,407 +9661,6 @@ For example, if you omit the `a` parameter, you'll receive a `400` response code To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). -# Seed Data with Custom CLI Script - -In this chapter, you'll learn how to seed data using a custom CLI script. - -## How to Seed Data - -To seed dummy data for development or demo purposes, use a custom CLI script. - -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. - -### Example: Seed Dummy Products - -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. - -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: - -```bash npm2yarn -npm install --save-dev @faker-js/faker -``` - -Then, create the file `src/scripts/demo-products.ts` with the following content: - -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) - - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) - - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 - - // TODO seed products -} -``` - -So far, in the script, you: - -- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. -- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. -- Initialize some default data to use when seeding the products next. - -Next, replace the `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const productsData = new Array(productsNum).fill(0).map((_, index) => { - const title = faker.commerce.product() + "_" + index - return { - title, - is_giftcard: true, - description: faker.commerce.productDescription(), - status: ProductStatus.PUBLISHED, - options: [ - { - title: "Size", - values: sizeOptions, - }, - { - title: "Color", - values: colorOptions, - }, - ], - images: [ - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - ], - variants: new Array(10).fill(0).map((_, variantIndex) => ({ - title: `${title} ${variantIndex}`, - sku: `variant-${variantIndex}${index}`, - prices: new Array(10).fill(0).map((_, priceIndex) => ({ - currency_code, - amount: 10 * priceIndex, - })), - options: { - Size: sizeOptions[Math.floor(Math.random() * 3)], - }, - })), - shipping_profile_id: "sp_123", - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } -}) - -// TODO seed products -``` - -You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. - -Then, replace the new `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) - -logger.info(`Seeded ${products.length} products.`) - -// TODO add inventory levels -``` - -You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. - -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") - -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) - -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) - -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) - -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) - -logger.info("Finished seeding inventory levels data.") -``` - -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. - -Then, you generate inventory levels for each inventory item, associating it with the first stock location. - -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. - -### Test Script - -To test out the script, run the following command in your project's directory: - -```bash -npx medusa exec ./src/scripts/demo-products.ts -``` - -This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. - - -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload - -When events are emitted, they’re emitted with a data payload. - -The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. - -For example: - -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" - -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} - -export const config: SubscriberConfig = { - event: "product.created", -} -``` - -The `event` object has the following properties: - -- data: (\`object\`) The data payload of the event. Its properties are different for each event. -- name: (string) The name of the triggered event. -- metadata: (\`object\`) Additional data and context of the emitted event. - -This logs the product ID received in the `product.created` event’s data payload to the console. - -{/* --- - -## List of Events with Data Payload - -Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} - - -# Emit Workflow and Service Events - -In this chapter, you'll learn about event types and how to emit an event in a service or workflow. - -## Event Types - -In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. - -There are two types of events in Medusa: - -1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. -2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. - -### Which Event Type to Use? - -**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. - -Some examples of workflow events: - -1. When a user creates a blog post and you're emitting an event to send a newsletter email. -2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. -3. When a customer purchases a digital product and you want to generate and send it to them. - -You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. - -Some examples of service events: - -1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. -2. When you're syncing data with a search engine. - -*** - -## Emit Event in a Workflow - -To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -const helloWorldWorkflow = createWorkflow( - "hello-world", - () => { - // ... - - emitEventStep({ - eventName: "custom.created", - data: { - id: "123", - // other data payload - }, - }) - } -) -``` - -The `emitEventStep` accepts an object having the following properties: - -- `eventName`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -In this example, you emit the event `custom.created` and pass in the data payload an ID property. - -### Test it Out - -If you execute the workflow, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are executed. - -*** - -## Emit Event in a Service - -To emit a service event: - -1. Resolve `event_bus` from the module's container in your service's constructor: - -### Extending Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["9"]} -import { IEventBusService } from "@medusajs/framework/types" -import { MedusaService } from "@medusajs/framework/utils" - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - super(...arguments) - this.eventBusService_ = event_bus - } -} -``` - -### Without Service Factory - -```ts title="src/modules/blog/service.ts" highlights={["6"]} -import { IEventBusService } from "@medusajs/framework/types" - -class BlogModuleService { - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } -} -``` - -2. Use the event bus service's `emit` method in the service's methods to emit an event: - -```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} -class BlogModuleService { - // ... - performAction() { - // TODO perform action - - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -} -``` - -The method accepts an object having the following properties: - -- `name`: The event's name. -- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. - -3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: - -```ts title="medusa-config.ts" highlights={depsHighlight} -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) -``` - -The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. - -That's how you can resolve it in your module's main service's constructor. - -### Test it Out - -If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. - -Any subscribers listening to the event are also executed. - - # Add Data Model Check Constraints In this chapter, you'll learn how to add check constraints to your data model. @@ -10246,6 +9845,46 @@ export default MyCustom This creates a unique composite index on the `name` and `age` properties. +# Infer Type of Data Model + +In this chapter, you'll learn how to infer the type of a data model. + +## How to Infer Type of Data Model? + +Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. + +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. + +For example: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../modules/blog/models/post" // relative path to the model + +export type Post = InferTypeOf +``` + +`InferTypeOf` accepts as a type argument the type of the data model. + +Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. + +You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: + +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../models/post" + +type Post = InferTypeOf + +class BlogModuleService extends MedusaService({ Post }) { + async doSomething(): Promise { + // ... + } +} +``` + + # Manage Relationships In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. @@ -10466,46 +10105,6 @@ const product = await blogModuleService.retrieveProducts( In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -# Infer Type of Data Model - -In this chapter, you'll learn how to infer the type of a data model. - -## How to Infer Type of Data Model? - -Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. - -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. - -For example: - -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../modules/blog/models/post" // relative path to the model - -export type Post = InferTypeOf -``` - -`InferTypeOf` accepts as a type argument the type of the data model. - -Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. - -You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: - -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../models/post" - -type Post = InferTypeOf - -class BlogModuleService extends MedusaService({ Post }) { - async doSomething(): Promise { - // ... - } -} -``` - - # Data Model Properties In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties. @@ -10858,104 +10457,6 @@ const posts = await blogModuleService.listPosts({ This retrieves records that include `New Products` in their `title` property. -# Migrations - -In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. - -## What is a Migration? - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -The migration's file has a class with two methods: - -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. - -*** - -## Generate Migration - -Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. - -For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: - -```bash -npx medusa db:generate blog -``` - -This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). - -*** - -## Write a Migration Manually - -You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. - -For example: - -```ts title="src/modules/blog/migrations/Migration202507021059.ts" -import { Migration } from "@mikro-orm/migrations" - -export class Migration202507021059 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } - -} -``` - -The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. - -In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. - -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. - -*** - -## Run the Migration - -To run your migration, run the following command: - -This command also syncs module links. If you don't want that, use the `--skip-links` option. - -```bash -npx medusa db:migrate -``` - -This reflects the changes in the database as implemented in the migration's `up` method. - -*** - -## Rollback the Migration - -To rollback or revert the last migration you ran for a module, run the following command: - -```bash -npx medusa db:rollback blog -``` - -This rolls back the last ran migration on the Blog Module. - -### Caution: Rollback Migration before Deleting - -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. - -So, always rollback the migration before deleting it. - -*** - -## More Database Commands - -To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). - - # Data Model Relationships In this chapter, you’ll learn how to define relationships between data models in your module. @@ -11251,6 +10752,2372 @@ The `cascades` method accepts an object. Its key is the operation’s name, such In the example above, when a store is deleted, its associated products are also deleted. +# Migrations + +In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. + +## What is a Migration? + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +The migration's file has a class with two methods: + +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. + +*** + +## Generate Migration + +Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. + +For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: + +```bash +npx medusa db:generate blog +``` + +This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). + +*** + +## Write a Migration Manually + +You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. + +For example: + +```ts title="src/modules/blog/migrations/Migration202507021059.ts" +import { Migration } from "@mikro-orm/migrations" + +export class Migration202507021059 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } + +} +``` + +The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. + +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. + +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. + +*** + +## Run the Migration + +To run your migration, run the following command: + +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate +``` + +This reflects the changes in the database as implemented in the migration's `up` method. + +*** + +## Rollback the Migration + +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback blog +``` + +This rolls back the last ran migration on the Blog Module. + +### Caution: Rollback Migration before Deleting + +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. + +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. + +*** + +## More Database Commands + +To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). + + +# Emit Workflow and Service Events + +In this chapter, you'll learn about event types and how to emit an event in a service or workflow. + +## Event Types + +In your customization, you can emit an event, then listen to it in a subscriber and perform an asynchronus action, such as send a notification or data to a third-party system. + +There are two types of events in Medusa: + +1. Workflow event: an event that's emitted in a workflow after a commerce feature is performed. For example, Medusa emits the `order.placed` event after a cart is completed. +2. Service event: an event that's emitted to track, trace, or debug processes under the hood. For example, you can emit an event with an audit trail. + +### Which Event Type to Use? + +**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. + +Some examples of workflow events: + +1. When a user creates a blog post and you're emitting an event to send a newsletter email. +2. When you finish syncing products to a third-party system and you want to notify the admin user of new products added. +3. When a customer purchases a digital product and you want to generate and send it to them. + +You should only go for a **service event** if you're emitting an event for processes under the hood that don't directly affect front-facing features. + +Some examples of service events: + +1. When you're tracing data manipulation and changes, and you want to track every time some custom data is changed. +2. When you're syncing data with a search engine. + +*** + +## Emit Event in a Workflow + +To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. + +For example: + +```ts highlights={highlights} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... + + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) +``` + +The `emitEventStep` accepts an object having the following properties: + +- `eventName`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. + +In this example, you emit the event `custom.created` and pass in the data payload an ID property. + +### Test it Out + +If you execute the workflow, the event is emitted and you can see it in your application's logs. + +Any subscribers listening to the event are executed. + +*** + +## Emit Event in a Service + +To emit a service event: + +1. Resolve `event_bus` from the module's container in your service's constructor: + +### Extending Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["9"]} +import { IEventBusService } from "@medusajs/framework/types" +import { MedusaService } from "@medusajs/framework/utils" + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + super(...arguments) + this.eventBusService_ = event_bus + } +} +``` + +### Without Service Factory + +```ts title="src/modules/blog/service.ts" highlights={["6"]} +import { IEventBusService } from "@medusajs/framework/types" + +class BlogModuleService { + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } +} +``` + +2. Use the event bus service's `emit` method in the service's methods to emit an event: + +```ts title="src/modules/blog/service.ts" highlights={serviceHighlights} +class BlogModuleService { + // ... + performAction() { + // TODO perform action + + this.eventBusService_.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } +} +``` + +The method accepts an object having the following properties: + +- `name`: The event's name. +- `data`: The data payload as an object. You can pass any properties in the object, and subscribers listening to the event will receive this data in the event's payload. + +3. By default, the Event Module's service isn't injected into your module's container. To add it to the container, pass it in the module's registration object in `medusa-config.ts` in the `dependencies` property: + +```ts title="medusa-config.ts" highlights={depsHighlight} +import { Modules } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) +``` + +The `dependencies` property accepts an array of module registration keys. The specified modules' main services are injected into the module's container. + +That's how you can resolve it in your module's main service's constructor. + +### Test it Out + +If you execute the `performAction` method of your service, the event is emitted and you can see it in your application's logs. + +Any subscribers listening to the event are also executed. + + +# Event Data Payload + +In this chapter, you'll learn how subscribers receive an event's data payload. + +## Access Event's Data Payload + +When events are emitted, they’re emitted with a data payload. + +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. + +For example: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" + +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `event` object has the following properties: + +- data: (\`object\`) The data payload of the event. Its properties are different for each event. +- name: (string) The name of the triggered event. +- metadata: (\`object\`) Additional data and context of the emitted event. + +This logs the product ID received in the `product.created` event’s data payload to the console. + +{/* --- + +## List of Events with Data Payload + +Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} + + +# Commerce Modules + +In this chapter, you'll learn about Medusa's Commerce Modules. + +## What is a Commerce Module? + +Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. + +Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. + +You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) + +The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. + +### List of Medusa's Commerce Modules + +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa. + +*** + +## Use Commerce Modules in Custom Flows + +Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a Commerce Module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. + +For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: + +```ts highlights={highlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") + + const [,count] = await productModuleService.listAndCountProducts() + + return new StepResponse(count) + } +) +``` + +Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features. + + +# Module Container + +In this chapter, you'll learn about the module's container and how to resolve resources in that container. + +Since modules are isolated, each module has a local container only used by the resources of that module. + +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. + +### List of Registered Resources + +Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). + +*** + +## Resolve Resources + +### Services + +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. + +For example: + +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export default class BlogModuleService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + + this.logger_.info("[BlogModuleService]: Hello World!") + } + + // ... +} +``` + +### Loader + +A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. + +For example: + +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("[helloWorldLoader]: Hello, World!") +} +``` + + +# Infrastructure Modules + +In this chapter, you’ll learn about Infrastructure Modules. + +## What is an Infrastructure Module? + +An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + +Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. + +*** + +## Infrastructure Module Types + +There are different Infrastructure Module types including: + +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) + +- Cache Module: Defines the caching mechanism or logic to cache computational results. +- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. +- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. +- File Module: Integrates a storage service to handle uploading and managing files. +- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. +- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. + +*** + +## Infrastructure Modules List + +Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. + + +# Seed Data with Custom CLI Script + +In this chapter, you'll learn how to seed data using a custom CLI script. + +## How to Seed Data + +To seed dummy data for development or demo purposes, use a custom CLI script. + +In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. + +### Example: Seed Dummy Products + +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. + +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: + +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` + +Then, create the file `src/scripts/demo-products.ts` with the following content: + +```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { ExecArgs } from "@medusajs/framework/types" +import { faker } from "@faker-js/faker" +import { + ContainerRegistrationKeys, + Modules, + ProductStatus, +} from "@medusajs/framework/utils" +import { + createInventoryLevelsWorkflow, + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export default async function seedDummyProducts({ + container, +}: ExecArgs) { + const salesChannelModuleService = container.resolve( + Modules.SALES_CHANNEL + ) + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + const query = container.resolve( + ContainerRegistrationKeys.QUERY + ) + + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) + + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 + + // TODO seed products +} +``` + +So far, in the script, you: + +- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. +- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. +- Initialize some default data to use when seeding the products next. + +Next, replace the `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const productsData = new Array(productsNum).fill(0).map((_, index) => { + const title = faker.commerce.product() + "_" + index + return { + title, + is_giftcard: true, + description: faker.commerce.productDescription(), + status: ProductStatus.PUBLISHED, + options: [ + { + title: "Size", + values: sizeOptions, + }, + { + title: "Color", + values: colorOptions, + }, + ], + images: [ + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + ], + variants: new Array(10).fill(0).map((_, variantIndex) => ({ + title: `${title} ${variantIndex}`, + sku: `variant-${variantIndex}${index}`, + prices: new Array(10).fill(0).map((_, priceIndex) => ({ + currency_code, + amount: 10 * priceIndex, + })), + options: { + Size: sizeOptions[Math.floor(Math.random() * 3)], + }, + })), + shipping_profile_id: "sp_123", + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } +}) + +// TODO seed products +``` + +You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. + +Then, replace the new `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) + +logger.info(`Seeded ${products.length} products.`) + +// TODO add inventory levels +``` + +You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. + +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") + +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) + +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: ["id"], +}) + +const inventoryLevels = inventoryItems.map((inventoryItem) => ({ + location_id: stockLocations[0].id, + stocked_quantity: 1000000, + inventory_item_id: inventoryItem.id, +})) + +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, +}) + +logger.info("Finished seeding inventory levels data.") +``` + +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. + +Then, you generate inventory levels for each inventory item, associating it with the first stock location. + +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. + +### Test Script + +To test out the script, run the following command in your project's directory: + +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` + +This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. + + +# Loaders + +In this chapter, you’ll learn about loaders and how to use them. + +## What is a Loader? + +When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup. + +In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. + +Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. + +Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). + +*** + +## How to Create a Loader? + +### 1. Implement Loader Function + +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. + +For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content: + +![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) + +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve("logger") + + logger.info("[HELLO MODULE] Just started the Medusa application!") +} +``` + +The loader file exports an async function, which is the function executed when the application loads. + +The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal. + +Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). + +### 2. Export Loader in Module Definition + +After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it. + +So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: + +```ts title="src/modules/hello/index.ts" +// other imports... +import helloWorldLoader from "./loaders/hello-world" + +export default Module("hello", { + // ... + loaders: [helloWorldLoader], +}) +``` + +The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts. + +### Test the Loader + +Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, you'll find the following message logged in the terminal: + +```plain +info: [HELLO MODULE] Just started the Medusa application! +``` + +This indicates that the loader in the `hello` module ran and logged this message. + +*** + +## When are Loaders Executed? + +When you start the Medusa application, it executes the loaders of all modules in their registration order. + +A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection. + +Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work. + +*** + +## Example: Register Custom MongoDB Connection + +As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module. + +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. + +### Prerequisites + +- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com) +- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver) + +To connect to the database, you create the following loader in your module: + +```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { asValue } from "awilix" +import { MongoClient } from "mongodb" + +type ModuleOptions = { + connection_url?: string + db_name?: string +} + +export default async function mongoConnectionLoader({ + container, + options, +}: LoaderOptions) { + if (!options.connection_url) { + throw new Error(`[MONGO MDOULE]: connection_url option is required.`) + } + if (!options.db_name) { + throw new Error(`[MONGO MDOULE]: db_name option is required.`) + } + const logger = container.resolve("logger") + + try { + const clientDb = ( + await (new MongoClient(options.connection_url)).connect() + ).db(options.db_name) + + logger.info("Connected to MongoDB") + + container.register( + "mongoClient", + asValue(clientDb) + ) + } catch (e) { + logger.error( + `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` + ) + } +} +``` + +The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example: + +```ts title="medusa-config.ts" highlights={optionHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/mongo", + options: { + connection_url: process.env.MONGO_CONNECTION_URL, + db_name: process.env.MONGO_DB_NAME, + }, + }, + ], +}) +``` + +Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options: + +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. + +In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options. + +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: + +1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client. +2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa. + +### Use Custom Registered Resource in Module's Service + +After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. + +For example: + +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" + +type InjectedDependencies = { + mongoClient: Db +} + +export default class MongoModuleService { + private mongoClient_: Db + + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } + + async createMovie({ title }: { + title: string + }) { + const moviesCol = this.mongoClient_.collection("movie") + + const insertedMovie = await moviesCol.insertOne({ + title, + }) + + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId, + }) + + return movie + } + + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") + + await moviesCol.deleteOne({ + _id: { + equals: id, + }, + }) + } +} +``` + +The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively. + +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: + +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" + +export const MONGO_MODULE = "mongo" + +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader], +}) +``` + +### Test it Out + +You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal: + +```bash +info: Connected to MongoDB +``` + +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. + + +# Modules Directory Structure + +In this document, you'll learn about the expected files and directories in your module. + +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) + +## index.ts + +The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## service.ts + +A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## Other Directories + +The following directories are optional and their content are explained more in the following chapters: + +- `models`: Holds the data models representing tables in the database. +- `migrations`: Holds the migration files used to reflect changes on the database. +- `loaders`: Holds the scripts to run on the Medusa application's start-up. + + +# Perform Database Operations in a Service + +In this chapter, you'll learn how to perform database operations in a module's service. + +This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead. + +## Run Queries + +[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations. + +Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager). + +So, to run database queries in a service: + +1. Add the `InjectManager` decorator to the method. +2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` + +For example, in your service, add the following methods: + +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + + @InjectManager() + async getCount( + @MedusaContext() sharedContext?: Context + ): Promise { + return await sharedContext?.manager?.count("my_custom") + } + + @InjectManager() + async getCountSql( + @MedusaContext() sharedContext?: Context + ): Promise { + const data = await sharedContext?.manager?.execute( + "SELECT COUNT(*) as num FROM my_custom" + ) + + return parseInt(data?.[0].num || 0) + } +} +``` + +You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. + +The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager). + +You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count. + +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. + +*** + +## Perform Database Operations + +There are two ways to perform database operations in transactional methods: + +1. Using the [data model repositories](#perform-database-operations-with-data-model-repositories) in your module. +2. Using the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) injected into the method's shared context. + +For both approaches, you must wrap the method performing the database operations in a transaction. + +### Wrap Database Operations in Transactions + +When performing database operations without using the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you must wrap the method performing the database operations in a transaction. + +To wrap database operations in a transaction, you create two methods: + +1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK. +2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section. + +Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application. + +For example: + +```ts highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext?.transactionManager + + // TODO: update the record + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `BlogModuleService` has two methods: + +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. + +You can then perform in the transactional method the database operations either using the [data model repository](#perform-database-operations-with-data-model-repositories) or the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager). + +#### Why Wrap a Transactional Method + +The variables in the transactional method (such as `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution. + +So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data. + +By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method. + +This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed. + +For example, the `update` method may call other methods than `update_` to perform other actions: + +```ts +// other imports... +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async update_( + // ... + ): Promise { + // ... + } + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + const newData = await this.update_(input, sharedContext) + + // example method that sends data to another system + await this.sendNewDataToSystem(newData) + + return newData + } +} +``` + +In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. + +#### Using Methods in Transactional Methods + +If your protected transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. + +For example: + +```ts highlights={anotherMethodHighlights} +// other imports... +import { + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + this.anotherMethod(sharedContext) + } +} +``` + +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. + +The `anotherMethod` now runs in the same transaction as the `update_` method. + +### Perform Database Operations with Data Model Repositories + +For every data model in your module, Medusa generates a data model repository that has methods to perform database operations. + +For example, if your module has a `Post` model, it has a `postRepository` in the container. + +The data model repository is a wrapper around the [entity manager](https://mikro-orm.io/api/knex/class/EntityManager) that provides a higher-level API for performing database operations. + +To use the low-level entity manager, use the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) instead. + +#### Resolve Data Model Repository + +When the Medusa application injects a data model repository into a module's container, it formats the registration name by: + +- Taking the data model's name that's passed as the first parameter of `model.define` +- Lower-casing the first character +- Suffixing the result with `Repository`. + +For example: + +- `Post` model: `postRepository` +- `My_Custom` model: `my_CustomRepository` + +So, to resolve a data model repository from a module's container, pass the expected registration name of the repository in the first parameter of the module's constructor (the container). + +For example: + +### Extending Service Factory + +```ts highlights={serviceFactoryRepoHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { InferTypeOf, DAL } from "@medusajs/framework/types" +import Post from "./models/post" + +type Post = InferTypeOf + +type InjectedDependencies = { + postRepository: DAL.RepositoryService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected postRepository_: DAL.RepositoryService + + constructor({ + postRepository, + }: InjectedDependencies) { + super(...arguments) + this.postRepository_ = postRepository + } +} + +export default BlogModuleService +``` + +### Without Service Factory + +```ts highlights={noServiceFactoryRepoHighlights} +import { InferTypeOf, DAL } from "@medusajs/framework/types" +import Post from "./models/post" + +type Post = InferTypeOf + +type InjectedDependencies = { + postRepository: DAL.RepositoryService +} + +class BlogModuleService { + protected postRepository_: DAL.RepositoryService + + constructor({ + postRepository, + }: InjectedDependencies) { + super(...arguments) + this.postRepository_ = postRepository + } +} + +export default BlogModuleService +``` + +You can then use the data model repository in your service to perform database operations. + +#### Data Model Repository Methods + +A data model repository has methods that allows you to create, update, and delete records, among other operations. + +To learn about the methods available in a data model repository, refer to the [Data Model Repository](https://docs.medusajs.com/resources/data-model-repository-reference/index.html.md) reference. + +### Perform Database Operations with the Transactional Entity Manager + +Your transactional method can use the transactional entity manager injected into the method's shared context to perform database operations. It's an instance of the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) class. + +To use an easier higher-level API focused on each data model, use the [data model repository](#perform-database-operations-with-data-model-repositories) instead. + +For example: + +```ts highlights={transactionalEntityManagerHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext?.transactionManager + await transactionManager?.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) + + // retrieve again + const updatedRecord = await transactionManager?.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) + + return updatedRecord + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `update_` method uses the transactional entity manager injected into the `sharedContext.transactionManager` property to perform the database operations. + +Find all available methods in the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) reference. + +*** + +## Configure Transactions with the Base Repository + +To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` class registered in your module's container. + +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. + +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. + +For example, resolve the `baseRepository` in your service's constructor: + +### Extending Service Factory + +```ts highlights={baseRepoHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" +import { DAL } from "@medusajs/framework/types" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected baseRepository_: DAL.RepositoryService + + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository + } +} + +export default BlogModuleService +``` + +### Without Service Factory + +```ts highlights={noServiceFactoryBaseRepoHighlights} +import { DAL } from "@medusajs/framework/types" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} + +class BlogModuleService { + protected baseRepository_: DAL.RepositoryService + + constructor({ baseRepository }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} + +export default BlogModuleService +``` + +Then, use it in the service's transactional methods: + +```ts highlights={repoHighlights} +// ... +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) + + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) + + return updatedRecord + }, + { + transaction: sharedContext?.transactionManager, + } + ) + } + + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} +``` + +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. + +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. + +The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager. + +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. + +### Transaction Options + +The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: + +1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. + +```ts highlights={[["24"]]} +// other imports... +import { EntityManager } from "@mikro-orm/knex" +import { + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" + +class BlogModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext?.transactionManager, + } + ) + } +} +``` + +2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be: + - `read committed` + - `read uncommitted` + - `snapshot` + - `repeatable read` + - `serializable` + +```ts highlights={[["25"]]} +// other imports... +import { + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" +import { IsolationLevel } from "@mikro-orm/core" + +class BlogModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED, + } + ) + } +} +``` + +3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions. + - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used. + +```ts highlights={[["24"]]} +// other imports... +import { + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class BlogModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false, + } + ) + } +} +``` + + +# Multiple Services in a Module + +In this chapter, you'll learn how to use multiple services in a module. + +## Module's Main and Internal Services + +A module has one main service only, which is the service exported in the module's definition. + +However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. + +*** + +## How to Add an Internal Service + +### 1. Create Service + +To add an internal service, create it in the `services` directory of your module. + +For example, create the file `src/modules/blog/services/client.ts` with the following content: + +```ts title="src/modules/blog/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } +} +``` + +### 2. Export Service in Index + +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. + +For example, create the file `src/modules/blog/services/index.ts` with the following content: + +```ts title="src/modules/blog/services/index.ts" +export * from "./client" +``` + +This exports the `ClientService`. + +### 3. Resolve Internal Service + +Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. + +For example, in your main service: + +```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" + +type InjectedDependencies = { + clientService: ClientService +} + +class BlogModuleService extends MedusaService({ + Post, +}){ + protected clientService_: ClientService + + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` + +You can now use your internal service in your main service. + +*** + +## Resolve Resources in Internal Service + +Resolve dependencies from your module's container in the constructor of your internal service. + +For example: + +```ts +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger +} + +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` + +*** + +## Access Module Options + +Your internal service can't access the module's options. + +To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. + +For example: + +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { BLOG_MODULE } from ".." + +export type InjectedDependencies = { + configModule: ConfigModule +} + +export class ClientService { + protected options: Record + + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[BLOG_MODULE] + + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } + } +} +``` + +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. + +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. + + +# Module Isolation + +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. + +- Modules can't access resources, such as services or data models, from other modules. +- Use Medusa's linking concepts, as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. + +## How are Modules Isolated? + +A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. + +For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. + +*** + +## Why are Modules Isolated + +Some of the module isolation's benefits include: + +- Integrate your module into any Medusa application without side-effects to your setup. +- Replace existing modules with your custom implementation, if your use case is drastically different. +- Use modules in other environments, such as Edge functions and Next.js apps. + +*** + +## How to Extend Data Model of Another Module? + +To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +*** + +## How to Use Services of Other Modules? + +If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. + +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. + +### Example + +For example, consider you have two modules: + +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). + +To sync brands from your application to the third-party system, create the following steps: + +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) + + const brands = await brandModuleService.listBrands() + + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` + +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. + +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) + } +) +``` + +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. + + +# Service Factory + +In this chapter, you’ll learn about what the service factory is and how to use it. + +## What is the Service Factory? + +Medusa provides a service factory that your module’s main service can extend. + +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. + +Your service provides data-management functionalities of your data models. + +*** + +## How to Extend the Service Factory? + +Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. + +For example, create the file `src/modules/blog/service.ts` with the following content: + +```ts title="src/modules/blog/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + // TODO implement custom methods +} + +export default BlogModuleService +``` + +### MedusaService Parameters + +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. + +In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. + +### Generated Methods + +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. + +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. + +For example, the following methods are generated for the service above: + +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) + +### listPosts + +### listPosts + +This method retrieves an array of records based on filters and pagination configurations. + +For example: + +```ts +const posts = await blogModuleService + .listPosts() + +// with filters +const posts = await blogModuleService + .listPosts({ + id: ["123"] + }) +``` + +### listAndCountPosts + +### retrievePost + +This method retrieves a record by its ID. + +For example: + +```ts +const post = await blogModuleService + .retrievePost("123") +``` + +### retrievePost + +### updatePosts + +This method updates and retrieves records of the data model. + +For example: + +```ts +const post = await blogModuleService + .updatePosts({ + id: "123", + title: "test" + }) + +// update multiple +const posts = await blogModuleService + .updatePosts([ + { + id: "123", + title: "test" + }, + { + id: "321", + title: "test 2" + }, + ]) + +// use filters +const posts = await blogModuleService + .updatePosts([ + { + selector: { + id: ["123", "321"] + }, + data: { + title: "test" + } + }, + ]) +``` + +### createPosts + +### softDeletePosts + +This method soft-deletes records using an array of IDs or an object of filters. + +For example: + +```ts +await blogModuleService.softDeletePosts("123") + +// soft-delete multiple +await blogModuleService.softDeletePosts([ + "123", "321" +]) + +// use filters +await blogModuleService.softDeletePosts({ + id: ["123", "321"] +}) +``` + +### updatePosts + +### deletePosts + +### softDeletePosts + +### restorePosts + +### Using a Constructor + +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. + +For example: + +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + constructor() { + super(...arguments) + } +} + +export default BlogModuleService +``` + + +# Create a Plugin + +In this chapter, you'll learn how to create a Medusa plugin and publish it. + +A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## 1. Create a Plugin Project + +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. + +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- `src/provider`: Contains [module providers](#create-module-providers). +- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. + +*** + +## 2. Prepare Plugin + +### Package Name + +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. + +For example: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} +``` + +### Package Keywords + +Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. + +Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. + +```json title="package.json" +{ + "keywords": [ + "medusa-plugin-integration", + "medusa-v2" + ], + // ... +} +``` + +In addition, make sure to use one of the following keywords based on your integration type: + +|Keyword|Description|Example| +|---|---|---| +|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| +|\`medusa-plugin-auth\`|Authentication service integration|Auth0| +|\`medusa-plugin-cms\`|CMS service integration|Contentful| +|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| +|\`medusa-plugin-payment\`|Payment service integration|PayPal| +|\`medusa-plugin-search\`|Search service integration|MeiliSearch| +|\`medusa-plugin-shipping\`|Shipping service integration|DHL| +|\`medusa-plugin-other\`|Other third-party integrations|Sentry| + +### Package Dependencies + +Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. + +In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. + +For example, assuming `2.5.0` is the latest Medusa version: + +```json title="package.json" +{ + "devDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/ui": "4.0.4", + "@medusajs/icons": "2.5.0", + "@swc/core": "1.5.7", + }, + "peerDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/ui": "4.0.3", + "@medusajs/icons": "2.5.0", + } +} +``` + +### Package Exports + +Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. + +In the `package.json` file, make sure your plugin has the following exports: + +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./admin": { + "import": "./.medusa/server/src/admin/index.mjs", + "require": "./.medusa/server/src/admin/index.js", + "default": "./.medusa/server/src/admin/index.js" + }, + "./*": "./.medusa/server/src/*.js" + } +} +``` + +Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. + +The plugin exports the following files and directories: + +- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. +- `./workflows`: The workflows exported in `./src/workflows/index.ts`. +- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. +- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. +- `./admin`: The admin extensions exported in `./src/admin/index.ts`. +- `./*`: Any other files in the plugin's `src` directory. + +*** + +## 3. Publish Plugin Locally for Development and Testing + +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. + +### Publish and Install Local Package + +### Prerequisites + +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, navigate to your Medusa application: + +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` + +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. + +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm install --save-dev yalc +``` + +After that, run the following Medusa CLI command to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` + +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. + +### Register Plugin in Medusa Application + +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. + +Add the plugin to the `plugins` array in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` + +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. + +#### Pass Module Options through Plugin + +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. + +For example: + +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). + +### Watch Plugin Changes During Development + +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. + +To do that, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +This command will: + +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. + +### Start Medusa Application + +You can start your Medusa application's development server to test out your plugin: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm run dev +``` + +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. + +*** + +## 4. Create Customizations in the Plugin + +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. + +- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) +- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) +- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) + +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). + +### Generating Migrations for Modules + +During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: + +```plain title="Plugin project" +DB_USERNAME=postgres +DB_PASSWORD=123... +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=db_name +``` + +You can add these environment variables in a `.env` file in your plugin project. The variables are: + +- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. +- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. +- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. +- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. +- `DB_NAME`: The name of the PostgreSQL database to connect to. + +Then, run the following command in your plugin project to generate migrations for the modules in your plugin: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +This command generates migrations for all modules in the plugin. + +Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: + +```bash title="Medusa application" +npx medusa db:migrate +``` + +The migrations in your application, including your plugin, will run and update the database. + +### Importing Module Resources + +In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. + +These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. + +For example, to import the plugin's workflow in your Medusa application: + +`@myorg/plugin-name` is the plugin package's name. + +```ts +import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" +import BlogModule from "@myorg/plugin-name/modules/blog" +// import other files created in plugin like ./src/types/blog.ts +import BlogType from "@myorg/plugin-name/types/blog" +``` + +### Create Module Providers + +The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. + +For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: + +`@myorg/plugin-name` is the plugin package's name. + +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. + +To learn how to create module providers, refer to the following guides: + +- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) +- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) +- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) +- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) + +*** + +## 5. Publish Plugin to NPM + +Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. + +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: + +```bash +npx medusa plugin:build +``` + +The command will compile an output in the `.medusa/server` directory. + +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: + +```bash +npm publish +``` + +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. + +### Install Public Plugin in Medusa Application + +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name +``` + +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). + +*** + +## Update a Published Plugin + +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). + +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. + +First, run the following command to change the version of the plugin: + +```bash +npm version +``` + +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. + +Then, re-run the same commands for publishing a plugin: + +```bash +npx medusa plugin:build +npm publish +``` + +This will publish an updated version of your plugin under a new version. + + # Add Columns to a Link Table In this chapter, you'll learn how to add custom columns to a link definition's table and manage them. @@ -11409,6 +13276,171 @@ await link.create({ ``` +# Module Options + +In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources. + +## What are Module Options? + +A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. + +*** + +## How to Pass Options to a Module? + +To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. + +For example: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property’s value is an object. You can pass any properties you want. + +### Pass Options to a Module in a Plugin + +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. + +For example: + +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in a plugin. + +*** + +## Access Module Options in Main Service + +The module’s main service receives the module options as a second parameter. + +For example: + +```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} + +export default class BlogModuleService extends MedusaService({ + Post, +}){ + protected options_: ModuleOptions + + constructor({}, options?: ModuleOptions) { + super(...arguments) + + this.options_ = options || { + capitalize: false, + } + } + + // ... +} +``` + +*** + +## Access Module Options in Loader + +The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. + +For example: + +```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" + +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} + +export default async function helloWorldLoader({ + options, +}: LoaderOptions) { + + console.log( + "[BLOG MODULE] Just started the Medusa application!", + options + ) +} +``` + +*** + +## Validate Module Options + +If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. + +So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. + +For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: + +```ts title="src/modules/blog/loaders/validate.ts" +import { LoaderOptions } from "@medusajs/framework/types" +import { MedusaError } from "@medusajs/framework/utils" + +// recommended to define type in another file +type ModuleOptions = { + apiKey?: string +} + +export default async function validationLoader({ + options, +}: LoaderOptions) { + if (!options.apiKey) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Hello Module requires an apiKey option." + ) + } +} +``` + +Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): + +```ts title="src/modules/blog/index.ts" +// other imports... +import validationLoader from "./loaders/validate" + +export const BLOG_MODULE = "blog" + +export default Module(BLOG_MODULE, { + // ... + loaders: [validationLoader], +}) +``` + +Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. + + # Link In this chapter, you’ll learn what Link is and how to use it to manage links. @@ -11674,561 +13706,43 @@ export default defineLink( ``` -# Query +# Service Constraints -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +This chapter lists constraints to keep in mind when creating a service. -## What is Query? +## Use Async Methods -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. -In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules. - -*** - -## Query Example - -For example, create the route `src/api/query/route.ts` with the following content: - -```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - - const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - }) - - res.json({ posts }) -} -``` - -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. - -Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: - -- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition. -- `fields`: An array of the data model’s properties to retrieve in the result. - -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: - -```json title="Returned Data" -{ - "data": [ - { - "id": "123", - "title": "My Post" - } - ] -} -``` - -*** - -## Querying the Graph - -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. - -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. - -*** - -## Retrieve Linked Records - -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: [ - "id", - "title", - "product.*", - ], -}) -``` - -`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property. - -For example: +For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: ```ts -const { data: posts } = await query.graph({ - entity: "post", - fields: [ - "id", - "title", - "product.id", - "product.title", - ], -}) +await blogModuleService.getMessage() ``` -In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`. +So, make sure your service's methods are always async to avoid unexpected errors or behavior. -### Retrieve List Link Records +```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" -If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: [ - "id", - "title", - "products.*", - ], -}) -``` - -In the example above, you retrieve all products linked to a post. - -### Apply Filters and Pagination on Linked Records - -Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead. - -As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table. - -A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. - -For example: - -```ts highlights={queryLinkTableHighlights} -import ProductPostLink from "../../../links/product-post" - -// ... - -const { data: productCustoms } = await query.graph({ - entity: ProductPostLink.entryPoint, - fields: ["*", "product.*", "post.*"], - pagination: { - take: 5, - skip: 0, - }, -}) -``` - -In the object passed to the `graph` method: - -- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table. -- You pass three items to the `field` property: - - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). - - `product.*` to retrieve the fields of a product record linked to a `Post` record. - - `post.*` to retrieve the fields of a `Post` record linked to a product record. - -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table. - -The returned `data` is similar to the following: - -```json title="Example Result" -[{ - "id": "123", - "product_id": "prod_123", - "post_id": "123", - "product": { - "id": "prod_123", - // other product fields... - }, - "post": { - "id": "123", - // other post fields... +class BlogModuleService extends MedusaService({ + Post, +}){ + // Don't + getMessage(): string { + return "Hello, World!" } -}] -``` -*** - -## Apply Filters - -```ts highlights={[["4"], ["5"], ["6"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - id: "post_123", - }, -}) -``` - -The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. - -In the example above, you filter the `post` records by the ID `post_123`. - -You can also filter by multiple values of a property. For example: - -```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - id: [ - "post_123", - "post_321", - ], - }, -}) -``` - -In the example above, you filter the `post` records by multiple IDs. - -Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution. - -### Advanced Query Filters - -Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records: - -- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`. -- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`. - -Both methods accepts a filter object that can be used to filter records. - -Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types. - -Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples. - -#### Filter by Not Matching a Value - -```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - title: { - $ne: null, - }, - }, -}) -``` - -In the example above, only posts that have a title are retrieved. - -#### Filter by Not Matching Multiple Values - -```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - title: { - $nin: ["My Post", "Another Post"], - }, - }, -}) -``` - -In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved. - -#### Filter by a Range - -```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]} -const startToday = new Date() -startToday.setHours(0, 0, 0, 0) - -const endToday = new Date() -endToday.setHours(23, 59, 59, 59) - -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - published_at: { - $gt: startToday, - $lt: endToday, - }, - }, -}) -``` - -In the example above, only posts that were published today are retrieved. - -#### Filter Text by Like Value - -This filter only applies to text-like properties, including `text`, `id`, and `enum` properties. - -```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - title: { - $like: "%My%", - }, - }, -}) -``` - -In the example above, only posts that have the word `My` in their title are retrieved. - -#### Filter a Relation's Property - -```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - author: { - name: "John", - }, - }, -}) -``` - -While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module). - -In the example above, only posts that have an author with the name `John` are retrieved. - -*** - -## Apply Pagination - -```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]} -const { - data: posts, - metadata: { count, take, skip } = {}, -} = await query.graph({ - entity: "post", - fields: ["id", "title"], - pagination: { - skip: 0, - take: 10, - }, -}) -``` - -The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. - -To paginate the returned records, pass the following properties to `pagination`: - -- `skip`: (required to apply pagination) The number of records to skip before fetching the results. -- `take`: The number of records to fetch. - -When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties: - -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. - -### Sort Records - -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - pagination: { - order: { - name: "DESC", - }, - }, -}) -``` - -Sorting doesn't work on fields of linked data models from other modules. - -To sort returned records, pass an `order` property to `pagination`. - -The `order` property is an object whose keys are property names, and values are either: - -- `ASC` to sort records by that property in ascending order. -- `DESC` to sort records by that property in descending order. - -*** - -## Configure Query to Throw Errors - -By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to configure Query to throw an error when no records are found. - -The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`. - -For example: - -```ts -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title"], - filters: { - id: "post_123", - }, -}, { - throwIfKeyNotFound: true, -}) -``` - -In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist. - -### Throw Error on Related Data Model - -The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist. - -For example: - -```ts -const { data: posts } = await query.graph({ - entity: "post", - fields: ["id", "title", "author.*"], - filters: { - id: "post_123", - author_id: "author_123", - }, -}, { - throwIfKeyNotFound: true, -}) -``` - -In the example above, Query throws an error either if no post is found with the ID `post_123` or if its found but its author ID isn't `author_123`. - -In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts. - -For example: - -```ts -const { data: posts } = await query.graph({ - entity: "author", - fields: ["id", "name", "posts.*"], - filters: { - id: "author_123", - posts: { - id: "post_123", - }, - }, -}, { - throwIfKeyNotFound: true, -}) -``` - -In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`. - -*** - -## Request Query Configurations - -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: - -- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -- Parses configurations that are received as query parameters to be passed to Query. - -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. - -### Step 1: Add Middleware - -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: - -```ts title="src/api/middlewares.ts" -import { - validateAndTransformQuery, - defineMiddlewares, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" - -export const GetCustomSchema = createFindParams() - -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "title", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], -}) -``` - -The `validateAndTransformQuery` accepts two parameters: - -1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: - 1. `fields`: The fields and relations to retrieve in the returned resources. - 2. `offset`: The number of items to skip before retrieving the returned items. - 3. `limit`: The maximum number of items to return. - 4. `order`: The fields to order the returned items by in ascending or descending order. -2. A Query configuration object. It accepts the following properties: - 1. `defaults`: An array of default fields and relations to retrieve in each resource. - 2. `isList`: A boolean indicating whether a list of items are returned in the response. - 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter. - 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`. - -### Step 2: Use Configurations in API Route - -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. - -The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object. - -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. - -For example, Create the file `src/api/customs/route.ts` with the following content: - -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) - - const { data: posts } = await query.graph({ - entity: "post", - ...req.queryConfig, - }) - - res.json({ posts: posts }) + // Do + async getMessage(): Promise { + return "Hello, World!" + } } + +export default BlogModuleService ``` -This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. - -In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request. - -### Test it Out - -To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware. - -```json title="Returned Data" -{ - "posts": [ - { - "id": "123", - "title": "test" - } - ] -} -``` - -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. - -Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. - # Query Context @@ -12962,2334 +14476,6 @@ If multiple posts have their `product_id` set to a product's ID, an array of pos [Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md). -# Commerce Modules - -In this chapter, you'll learn about Medusa's Commerce Modules. - -## What is a Commerce Module? - -Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. - -Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. - -You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) - -The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. - -### List of Medusa's Commerce Modules - -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa. - -*** - -## Use Commerce Modules in Custom Flows - -Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a Commerce Module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. - -For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: - -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" - -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") - - const [,count] = await productModuleService.listAndCountProducts() - - return new StepResponse(count) - } -) -``` - -Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features. - - -# Module Container - -In this chapter, you'll learn about the module's container and how to resolve resources in that container. - -Since modules are isolated, each module has a local container only used by the resources of that module. - -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. - -### List of Registered Resources - -Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). - -*** - -## Resolve Resources - -### Services - -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. - -For example: - -```ts highlights={[["4"], ["10"]]} -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export default class BlogModuleService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - - this.logger_.info("[BlogModuleService]: Hello World!") - } - - // ... -} -``` - -### Loader - -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. - -For example: - -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" - -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) - - logger.info("[helloWorldLoader]: Hello, World!") -} -``` - - -# Infrastructure Modules - -In this chapter, you’ll learn about Infrastructure Modules. - -## What is an Infrastructure Module? - -An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. - -Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. - -*** - -## Infrastructure Module Types - -There are different Infrastructure Module types including: - -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) - -- Cache Module: Defines the caching mechanism or logic to cache computational results. -- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. -- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. -- File Module: Integrates a storage service to handle uploading and managing files. -- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. -- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. - -*** - -## Infrastructure Modules List - -Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. - - -# Perform Database Operations in a Service - -In this chapter, you'll learn how to perform database operations in a module's service. - -This chapter is intended for more advanced database use-cases where you need more control over queries and operations. For basic database operations, such as creating or retrieving data of a model, use the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) instead. - -## Run Queries - -[MikroORM's entity manager](https://mikro-orm.io/docs/entity-manager) is a class that has methods to run queries on the database and perform operations. - -Medusa provides an `InjectManager` decorator from the Modules SDK that injects a service's method with a [forked entity manager](https://mikro-orm.io/docs/identity-map#forking-entity-manager). - -So, to run database queries in a service: - -1. Add the `InjectManager` decorator to the method. -2. Add as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. This context holds database-related context, including the manager injected by `InjectManager` - -For example, in your service, add the following methods: - -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - - @InjectManager() - async getCount( - @MedusaContext() sharedContext?: Context - ): Promise { - return await sharedContext?.manager?.count("my_custom") - } - - @InjectManager() - async getCountSql( - @MedusaContext() sharedContext?: Context - ): Promise { - const data = await sharedContext?.manager?.execute( - "SELECT COUNT(*) as num FROM my_custom" - ) - - return parseInt(data?.[0].num || 0) - } -} -``` - -You add two methods `getCount` and `getCountSql` that have the `InjectManager` decorator. Each of the methods also accept the `sharedContext` parameter which has the `MedusaContext` decorator. - -The entity manager is injected to the `sharedContext.manager` property, which is an instance of [EntityManager from the @mikro-orm/knex package](https://mikro-orm.io/api/knex/class/EntityManager). - -You use the manager in the `getCount` method to retrieve the number of records in a table, and in the `getCountSql` to run a PostgreSQL query that retrieves the count. - -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. - -*** - -## Perform Database Operations - -There are two ways to perform database operations in transactional methods: - -1. Using the [data model repositories](#perform-database-operations-with-data-model-repositories) in your module. -2. Using the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) injected into the method's shared context. - -For both approaches, you must wrap the method performing the database operations in a transaction. - -### Wrap Database Operations in Transactions - -When performing database operations without using the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), you must wrap the method performing the database operations in a transaction. - -To wrap database operations in a transaction, you create two methods: - -1. A private or protected method that's wrapped in a transaction. To wrap it in a transaction, you use the `InjectTransactionManager` decorator from the Modules SDK. -2. A public method that calls the transactional method. You use on it the `InjectManager` decorator as explained in the previous section. - -Both methods must accept as a last parameter an optional `sharedContext` parameter that has the `MedusaContext` decorator from the Modules SDK. It holds database-related contexts passed through the Medusa application. - -For example: - -```ts highlights={opHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - const transactionManager = sharedContext?.transactionManager - - // TODO: update the record - } - - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - return await this.update_(input, sharedContext) - } -} -``` - -The `BlogModuleService` has two methods: - -- A protected `update_` that performs the database operations inside a transaction. -- A public `update` that executes the transactional protected method. - -You can then perform in the transactional method the database operations either using the [data model repository](#perform-database-operations-with-data-model-repositories) or the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager). - -#### Why Wrap a Transactional Method - -The variables in the transactional method (such as `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution. - -So, if in your method you perform database operations, then use their result to perform other actions, such as connecting to a third-party service, you'll be working with uncommitted data. - -By placing only the database operations in a method that has the `InjectTransactionManager` and using it in a wrapper method, the wrapper method receives the committed result of the transactional method. - -This is also useful if you perform heavy data normalization outside of the database operations. In that case, you don't hold the transaction for a longer time than needed. - -For example, the `update` method may call other methods than `update_` to perform other actions: - -```ts -// other imports... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - protected async update_( - // ... - ): Promise { - // ... - } - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - const newData = await this.update_(input, sharedContext) - - // example method that sends data to another system - await this.sendNewDataToSystem(newData) - - return newData - } -} -``` - -In this case, only the `update_` method is wrapped in a transaction. The returned value `newData` holds the committed result, which can be used for other operations, such as passed to a `sendNewDataToSystem` method. - -#### Using Methods in Transactional Methods - -If your protected transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. - -For example: - -```ts highlights={anotherMethodHighlights} -// other imports... -import { - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - this.anotherMethod(sharedContext) - } -} -``` - -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. - -The `anotherMethod` now runs in the same transaction as the `update_` method. - -### Perform Database Operations with Data Model Repositories - -For every data model in your module, Medusa generates a data model repository that has methods to perform database operations. - -For example, if your module has a `Post` model, it has a `postRepository` in the container. - -The data model repository is a wrapper around the [entity manager](https://mikro-orm.io/api/knex/class/EntityManager) that provides a higher-level API for performing database operations. - -To use the low-level entity manager, use the [transactional entity manager](#perform-database-operations-with-the-transactional-entity-manager) instead. - -#### Resolve Data Model Repository - -When the Medusa application injects a data model repository into a module's container, it formats the registration name by: - -- Taking the data model's name that's passed as the first parameter of `model.define` -- Lower-casing the first character -- Suffixing the result with `Repository`. - -For example: - -- `Post` model: `postRepository` -- `My_Custom` model: `my_CustomRepository` - -So, to resolve a data model repository from a module's container, pass the expected registration name of the repository in the first parameter of the module's constructor (the container). - -For example: - -### Extending Service Factory - -```ts highlights={serviceFactoryRepoHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { InferTypeOf, DAL } from "@medusajs/framework/types" -import Post from "./models/post" - -type Post = InferTypeOf - -type InjectedDependencies = { - postRepository: DAL.RepositoryService -} - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected postRepository_: DAL.RepositoryService - - constructor({ - postRepository, - }: InjectedDependencies) { - super(...arguments) - this.postRepository_ = postRepository - } -} - -export default BlogModuleService -``` - -### Without Service Factory - -```ts highlights={noServiceFactoryRepoHighlights} -import { InferTypeOf, DAL } from "@medusajs/framework/types" -import Post from "./models/post" - -type Post = InferTypeOf - -type InjectedDependencies = { - postRepository: DAL.RepositoryService -} - -class BlogModuleService { - protected postRepository_: DAL.RepositoryService - - constructor({ - postRepository, - }: InjectedDependencies) { - super(...arguments) - this.postRepository_ = postRepository - } -} - -export default BlogModuleService -``` - -You can then use the data model repository in your service to perform database operations. - -#### Data Model Repository Methods - -A data model repository has methods that allows you to create, update, and delete records, among other operations. - -To learn about the methods available in a data model repository, refer to the [Data Model Repository](https://docs.medusajs.com/resources/data-model-repository-reference/index.html.md) reference. - -### Perform Database Operations with the Transactional Entity Manager - -Your transactional method can use the transactional entity manager injected into the method's shared context to perform database operations. It's an instance of the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) class. - -To use an easier higher-level API focused on each data model, use the [data model repository](#perform-database-operations-with-data-model-repositories) instead. - -For example: - -```ts highlights={transactionalEntityManagerHighlights} -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - const transactionManager = sharedContext?.transactionManager - await transactionManager?.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) - - // retrieve again - const updatedRecord = await transactionManager?.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) - - return updatedRecord - } - - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - return await this.update_(input, sharedContext) - } -} -``` - -The `update_` method uses the transactional entity manager injected into the `sharedContext.transactionManager` property to perform the database operations. - -Find all available methods in the [MikroORM EntityManager](https://mikro-orm.io/api/knex/class/EntityManager) reference. - -*** - -## Configure Transactions with the Base Repository - -To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` class registered in your module's container. - -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. - -The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. - -For example, resolve the `baseRepository` in your service's constructor: - -### Extending Service Factory - -```ts highlights={baseRepoHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" -import { DAL } from "@medusajs/framework/types" - -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected baseRepository_: DAL.RepositoryService - - constructor({ baseRepository }: InjectedDependencies) { - super(...arguments) - this.baseRepository_ = baseRepository - } -} - -export default BlogModuleService -``` - -### Without Service Factory - -```ts highlights={noServiceFactoryBaseRepoHighlights} -import { DAL } from "@medusajs/framework/types" - -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} - -class BlogModuleService { - protected baseRepository_: DAL.RepositoryService - - constructor({ baseRepository }: InjectedDependencies) { - this.baseRepository_ = baseRepository - } -} - -export default BlogModuleService -``` - -Then, use it in the service's transactional methods: - -```ts highlights={repoHighlights} -// ... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - await transactionManager.nativeUpdate( - "my_custom", - { - id: input.id, - }, - { - name: input.name, - } - ) - - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) - - return updatedRecord - }, - { - transaction: sharedContext?.transactionManager, - } - ) - } - - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - return await this.update_(input, sharedContext) - } -} -``` - -The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. - -The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. - -The `baseRepository_.transaction` method also receives as a second parameter an object of options. You must pass in it the `transaction` property and set its value to the `sharedContext.transactionManager` property so that the function wrapped in the transaction uses the injected transaction manager. - -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. - -### Transaction Options - -The second parameter of the `baseRepository_.transaction` method is an object of options that accepts the following properties: - -1. `transaction`: Set the transactional entity manager passed to the function. You must provide this option as explained in the previous section. - -```ts highlights={[["24"]]} -// other imports... -import { EntityManager } from "@mikro-orm/knex" -import { - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" - -class BlogModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - transaction: sharedContext?.transactionManager, - } - ) - } -} -``` - -2. `isolationLevel`: Sets the transaction's [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html). Its values can be: - - `read committed` - - `read uncommitted` - - `snapshot` - - `repeatable read` - - `serializable` - -```ts highlights={[["25"]]} -// other imports... -import { - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" -import { IsolationLevel } from "@mikro-orm/core" - -class BlogModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - isolationLevel: IsolationLevel.READ_COMMITTED, - } - ) - } -} -``` - -3. `enableNestedTransactions`: (default: `false`) whether to allow using nested transactions. - - If `transaction` is provided and this is disabled, the manager in `transaction` is re-used. - -```ts highlights={[["24"]]} -// other imports... -import { - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" - -class BlogModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - enableNestedTransactions: false, - } - ) - } -} -``` - - -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. - -- Modules can't access resources, such as services or data models, from other modules. -- Use Medusa's linking concepts, as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), to extend a module's data models and retrieve data across modules. - -## How are Modules Isolated? - -A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. - -For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. - -*** - -## Why are Modules Isolated - -Some of the module isolation's benefits include: - -- Integrate your module into any Medusa application without side-effects to your setup. -- Replace existing modules with your custom implementation, if your use case is drastically different. -- Use modules in other environments, such as Edge functions and Next.js apps. - -*** - -## How to Extend Data Model of Another Module? - -To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -*** - -## How to Use Services of Other Modules? - -If you're building a feature that uses functionalities from different modules, use a workflow whose steps resolve the modules' services to perform these functionalities. - -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. - -### Example - -For example, consider you have two modules: - -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). - -To sync brands from your application to the third-party system, create the following steps: - -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) - - const brands = await brandModuleService.listBrands() - - return new StepResponse(brands) - } -) - -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - const cmsBrands = await cmsModuleService.createBrands(brands) - - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) - - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) -``` - -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. - -Then, create the following workflow that uses these steps: - -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() - - createBrandsInCmsStep({ brands }) - } -) -``` - -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. - - -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - -# Loaders - -In this chapter, you’ll learn about loaders and how to use them. - -## What is a Loader? - -When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup. - -In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. - -Loaders are useful to register custom resources, such as database connections, in the [module's container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md), which is similar to the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) but includes only [resources available to the module](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). Modules are isolated, so they can't access resources outside of them, such as a service in another module. - -Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), and check out [this reference for the list of resources in the module's container](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). - -*** - -## How to Create a Loader? - -### 1. Implement Loader Function - -You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. - -For example, consider you have a `hello` module, you can create a loader at `src/modules/hello/loaders/hello-world.ts` with the following content: - -![Example of loader file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732865671/Medusa%20Book/loader-dir-overview_eg6vtu.jpg) - -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -```ts title="src/modules/hello/loaders/hello-world.ts" -import { - LoaderOptions, -} from "@medusajs/framework/types" - -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve("logger") - - logger.info("[HELLO MODULE] Just started the Medusa application!") -} -``` - -The loader file exports an async function, which is the function executed when the application loads. - -The function receives an object parameter that has a `container` property, which is the module's container that you can use to resolve resources from. In this example, you resolve the Logger utility to log a message in the terminal. - -Find the list of resources in the module's container in [this reference](https://docs.medusajs.com/resources/medusa-container-resources#module-container-resources/index.html.md). - -### 2. Export Loader in Module Definition - -After implementing the loader, you must export it in the module's definition in the `index.ts` file at the root of the module's directory. Otherwise, the Medusa application will not run it. - -So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: - -```ts title="src/modules/hello/index.ts" -// other imports... -import helloWorldLoader from "./loaders/hello-world" - -export default Module("hello", { - // ... - loaders: [helloWorldLoader], -}) -``` - -The second parameter of the `Module` function accepts a `loaders` property whose value is an array of loader functions. The Medusa application will execute these functions when it starts. - -### Test the Loader - -Assuming your module is [added to Medusa's configuration](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you can test the loader by starting the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, you'll find the following message logged in the terminal: - -```plain -info: [HELLO MODULE] Just started the Medusa application! -``` - -This indicates that the loader in the `hello` module ran and logged this message. - -*** - -## When are Loaders Executed? - -When you start the Medusa application, it executes the loaders of all modules in their registration order. - -A loader is executed before the module's main service is instantiated. So, you can use loaders to register in the module's container resources that you want to use in the module's service. For example, you can register a database connection. - -Loaders are also useful to only load a module if a certain condition is met. For example, if you try to connect to a database in a loader but the connection fails, you can throw an error in the loader to prevent the module from being loaded. This is useful if your module depends on an external service to work. - -*** - -## Example: Register Custom MongoDB Connection - -As mentioned in this chapter's introduction, loaders are most useful when you need to register a custom resource in the module's container to re-use it in other customizations in the module. - -Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. - -### Prerequisites - -- [MongoDB database that you can connect to from a local machine.](https://www.mongodb.com) -- [Install the MongoDB SDK in your Medusa application.](https://www.mongodb.com/docs/drivers/node/current/quick-start/download-and-install/#install-the-node.js-driver) - -To connect to the database, you create the following loader in your module: - -```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} -import { LoaderOptions } from "@medusajs/framework/types" -import { asValue } from "awilix" -import { MongoClient } from "mongodb" - -type ModuleOptions = { - connection_url?: string - db_name?: string -} - -export default async function mongoConnectionLoader({ - container, - options, -}: LoaderOptions) { - if (!options.connection_url) { - throw new Error(`[MONGO MDOULE]: connection_url option is required.`) - } - if (!options.db_name) { - throw new Error(`[MONGO MDOULE]: db_name option is required.`) - } - const logger = container.resolve("logger") - - try { - const clientDb = ( - await (new MongoClient(options.connection_url)).connect() - ).db(options.db_name) - - logger.info("Connected to MongoDB") - - container.register( - "mongoClient", - asValue(clientDb) - ) - } catch (e) { - logger.error( - `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` - ) - } -} -``` - -The loader function accepts in its object parameter an `options` property, which is the options passed to the module in Medusa's configurations. For example: - -```ts title="medusa-config.ts" highlights={optionHighlights} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/mongo", - options: { - connection_url: process.env.MONGO_CONNECTION_URL, - db_name: process.env.MONGO_DB_NAME, - }, - }, - ], -}) -``` - -Passing options is useful when your module needs informations like connection URLs or API keys, as it ensures your module can be re-usable across applications. For the MongoDB Module, you expect two options: - -- `connection_url`: the URL to connect to the MongoDB database. -- `db_name`: The name of the database to connect to. - -In the loader, you check first that these options are set before proceeding. Then, you create an instance of the MongoDB client and connect to the database specified in the options. - -After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: - -1. The key to register the resource under, which in this case is `mongoClient`. You'll use this name later to resolve the client. -2. The resource to register in the container, which is the MongoDB client you created. However, you don't pass the resource as-is. Instead, you need to use an `asValue` function imported from the [awilix package](https://github.com/jeffijoe/awilix), which is the package used to implement the container functionality in Medusa. - -### Use Custom Registered Resource in Module's Service - -After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. - -For example: - -```ts title="src/modules/mongo/service.ts" -import type { Db } from "mongodb" - -type InjectedDependencies = { - mongoClient: Db -} - -export default class MongoModuleService { - private mongoClient_: Db - - constructor({ mongoClient }: InjectedDependencies) { - this.mongoClient_ = mongoClient - } - - async createMovie({ title }: { - title: string - }) { - const moviesCol = this.mongoClient_.collection("movie") - - const insertedMovie = await moviesCol.insertOne({ - title, - }) - - const movie = await moviesCol.findOne({ - _id: insertedMovie.insertedId, - }) - - return movie - } - - async deleteMovie(id: string) { - const moviesCol = this.mongoClient_.collection("movie") - - await moviesCol.deleteOne({ - _id: { - equals: id, - }, - }) - } -} -``` - -The service `MongoModuleService` resolves the `mongoClient` resource you registered in the loader and sets it as a class property. You then use it in the `createMovie` and `deleteMovie` methods, which create and delete a document in a `movie` collection in the MongoDB database, respectively. - -Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: - -```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} -import { Module } from "@medusajs/framework/utils" -import MongoModuleService from "./service" -import mongoConnectionLoader from "./loaders/connection" - -export const MONGO_MODULE = "mongo" - -export default Module(MONGO_MODULE, { - service: MongoModuleService, - loaders: [mongoConnectionLoader], -}) -``` - -### Test it Out - -You can test the connection out by starting the Medusa application. If it's successful, you'll see the following message logged in the terminal: - -```bash -info: Connected to MongoDB -``` - -You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. - - -# Multiple Services in a Module - -In this chapter, you'll learn how to use multiple services in a module. - -## Module's Main and Internal Services - -A module has one main service only, which is the service exported in the module's definition. - -However, you may use other services in your module to better organize your code or split functionalities. These are called internal services that can be resolved within your module, but not in external resources. - -*** - -## How to Add an Internal Service - -### 1. Create Service - -To add an internal service, create it in the `services` directory of your module. - -For example, create the file `src/modules/blog/services/client.ts` with the following content: - -```ts title="src/modules/blog/services/client.ts" -export class ClientService { - async getMessage(): Promise { - return "Hello, World!" - } -} -``` - -### 2. Export Service in Index - -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - -For example, create the file `src/modules/blog/services/index.ts` with the following content: - -```ts title="src/modules/blog/services/index.ts" -export * from "./client" -``` - -This exports the `ClientService`. - -### 3. Resolve Internal Service - -Internal services exported in the `services/index.ts` file of your module are now registered in the container and can be resolved in other services in the module as well as loaders. - -For example, in your main service: - -```ts title="src/modules/blog/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" - -type InjectedDependencies = { - clientService: ClientService -} - -class BlogModuleService extends MedusaService({ - Post, -}){ - protected clientService_: ClientService - - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } -} -``` - -You can now use your internal service in your main service. - -*** - -## Resolve Resources in Internal Service - -Resolve dependencies from your module's container in the constructor of your internal service. - -For example: - -```ts -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export class ClientService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - } -} -``` - -*** - -## Access Module Options - -Your internal service can't access the module's options. - -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. - -For example: - -```ts -import { ConfigModule } from "@medusajs/framework/types" -import { BLOG_MODULE } from ".." - -export type InjectedDependencies = { - configModule: ConfigModule -} - -export class ClientService { - protected options: Record - - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[BLOG_MODULE] - - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options - } - } -} -``` - -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. - -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. - - -# Module Options - -In this chapter, you’ll learn about passing options to your module from the Medusa application’s configurations and using them in the module’s resources. - -## What are Module Options? - -A module can receive options to customize or configure its functionality. For example, if you’re creating a module that integrates a third-party service, you’ll want to receive the integration credentials in the options rather than adding them directly in your code. - -*** - -## How to Pass Options to a Module? - -To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. - -For example: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - options: { - capitalize: true, - }, - }, - ], -}) -``` - -The `options` property’s value is an object. You can pass any properties you want. - -### Pass Options to a Module in a Plugin - -If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. - -For example: - -```ts title="medusa-config.ts" -import { defineConfig } from "@medusajs/framework/utils" -module.exports = defineConfig({ - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - capitalize: true, - }, - }, - ], -}) -``` - -The `options` property in the plugin configuration is passed to all modules in a plugin. - -*** - -## Access Module Options in Main Service - -The module’s main service receives the module options as a second parameter. - -For example: - -```ts title="src/modules/blog/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} - -export default class BlogModuleService extends MedusaService({ - Post, -}){ - protected options_: ModuleOptions - - constructor({}, options?: ModuleOptions) { - super(...arguments) - - this.options_ = options || { - capitalize: false, - } - } - - // ... -} -``` - -*** - -## Access Module Options in Loader - -The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. - -For example: - -```ts title="src/modules/blog/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" - -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} - -export default async function helloWorldLoader({ - options, -}: LoaderOptions) { - - console.log( - "[BLOG MODULE] Just started the Medusa application!", - options - ) -} -``` - -*** - -## Validate Module Options - -If you expect a certain option and want to throw an error if it's not provided or isn't valid, it's recommended to perform the validation in a loader. The module's service is only instantiated when it's used, whereas the loader runs the when the Medusa application starts. - -So, by performing the validation in the loader, you ensure you can throw an error at an early point, rather than when the module is used. - -For example, to validate that the Hello Module received an `apiKey` option, create the loader `src/modules/loaders/validate.ts`: - -```ts title="src/modules/blog/loaders/validate.ts" -import { LoaderOptions } from "@medusajs/framework/types" -import { MedusaError } from "@medusajs/framework/utils" - -// recommended to define type in another file -type ModuleOptions = { - apiKey?: string -} - -export default async function validationLoader({ - options, -}: LoaderOptions) { - if (!options.apiKey) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Hello Module requires an apiKey option." - ) - } -} -``` - -Then, export the loader in the module's definition file, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md): - -```ts title="src/modules/blog/index.ts" -// other imports... -import validationLoader from "./loaders/validate" - -export const BLOG_MODULE = "blog" - -export default Module(BLOG_MODULE, { - // ... - loaders: [validationLoader], -}) -``` - -Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. - - -# Service Constraints - -This chapter lists constraints to keep in mind when creating a service. - -## Use Async Methods - -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. - -For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: - -```ts -await blogModuleService.getMessage() -``` - -So, make sure your service's methods are always async to avoid unexpected errors or behavior. - -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } - - // Do - async getMessage(): Promise { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - - -# Service Factory - -In this chapter, you’ll learn about what the service factory is and how to use it. - -## What is the Service Factory? - -Medusa provides a service factory that your module’s main service can extend. - -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. - -Your service provides data-management functionalities of your data models. - -*** - -## How to Extend the Service Factory? - -Medusa provides the service factory as a `MedusaService` function your service extends. The function creates and returns a service class with generated data-management methods. - -For example, create the file `src/modules/blog/service.ts` with the following content: - -```ts title="src/modules/blog/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // TODO implement custom methods -} - -export default BlogModuleService -``` - -### MedusaService Parameters - -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. - -In the example above, since the `BlogModuleService` extends `MedusaService`, it has methods to manage the `Post` data model, such as `createPosts`. - -### Generated Methods - -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. - -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. - -For example, the following methods are generated for the service above: - -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) - -### listPosts - -### listPosts - -This method retrieves an array of records based on filters and pagination configurations. - -For example: - -```ts -const posts = await blogModuleService - .listPosts() - -// with filters -const posts = await blogModuleService - .listPosts({ - id: ["123"] - }) -``` - -### listAndCountPosts - -### retrievePost - -This method retrieves a record by its ID. - -For example: - -```ts -const post = await blogModuleService - .retrievePost("123") -``` - -### retrievePost - -### updatePosts - -This method updates and retrieves records of the data model. - -For example: - -```ts -const post = await blogModuleService - .updatePosts({ - id: "123", - title: "test" - }) - -// update multiple -const posts = await blogModuleService - .updatePosts([ - { - id: "123", - title: "test" - }, - { - id: "321", - title: "test 2" - }, - ]) - -// use filters -const posts = await blogModuleService - .updatePosts([ - { - selector: { - id: ["123", "321"] - }, - data: { - title: "test" - } - }, - ]) -``` - -### createPosts - -### softDeletePosts - -This method soft-deletes records using an array of IDs or an object of filters. - -For example: - -```ts -await blogModuleService.softDeletePosts("123") - -// soft-delete multiple -await blogModuleService.softDeletePosts([ - "123", "321" -]) - -// use filters -await blogModuleService.softDeletePosts({ - id: ["123", "321"] -}) -``` - -### updatePosts - -### deletePosts - -### softDeletePosts - -### restorePosts - -### Using a Constructor - -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. - -For example: - -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - constructor() { - super(...arguments) - } -} - -export default BlogModuleService -``` - - -# Scheduled Jobs Number of Executions - -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. - -## numberOfExecutions Option - -The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. - -For example: - -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} - -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` - -The above scheduled job has the `numberOfExecutions` configuration set to `3`. - -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. - - -# Expose a Workflow Hook - -In this chapter, you'll learn how to expose a hook in your workflow. - -## When to Expose a Hook - -Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. - -Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. - -*** - -## How to Expose a Hook in a Workflow? - -To expose a hook in your workflow, use `createHook` from the Workflows SDK. - -For example: - -```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} -import { - createStep, - createHook, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { createProductStep } from "./steps/create-product" - -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) - - return new WorkflowResponse(product, { - hooks: [productCreatedHook], - }) - } -) -``` - -The `createHook` function accepts two parameters: - -1. The first is a string indicating the hook's name. You use this to consume the hook later. -2. The second is the input to pass to the hook handler. - -The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. - -### How to Consume the Hook? - -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: - -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" - -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) -``` - -The hook is available on the workflow's `hooks` property using its name `productCreated`. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - - -# Conditions in Workflows with When-Then - -In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. - -## Why If-Conditions Aren't Allowed in Workflows? - -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - -So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. - -Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. - -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. - -*** - -## How to use When-Then? - -The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... - -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) - - // executed without condition - const anotherStepResult = anotherStep(result) - - return new WorkflowResponse( - anotherStepResult - ) - } -) -``` - -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters - -`when` accepts the following parameters: - -1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. - -### Then Parameters - -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. - -The callback function is only executed if `when`'s second parameter function returns a `true` value. - -*** - -## Implementing If-Else with When-Then - -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. - -For example: - -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) - - // ... - } -) -``` - -In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. - -*** - -## Specify Name for When-Then - -Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: - -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` - -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. - -However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. - -You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: - -```ts highlights={nameHighlights} -const { isActive } = when( - "check-is-active", - input, - (input) => { - return input.is_active - } -).then(() => { - const isActive = isActiveStep() - - return { - isActive, - } -}) -``` - -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: - -1. A unique name to be assigned to the `when-then` block. -2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -3. A function that returns a boolean indicating whether to execute the action in `then`. - -The second and third parameters are the same as the parameters you previously passed to `when`. - - -# Create a Plugin - -In this chapter, you'll learn how to create a Medusa plugin and publish it. - -A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## 1. Create a Plugin Project - -Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. - -Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: - -```bash -npx create-medusa-app my-plugin --plugin -``` - -This will create a new Medusa plugin project in the `my-plugin` directory. - -### Plugin Directory Structure - -After the installation is done, the plugin structure will look like this: - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -- `src/`: Contains the Medusa customizations. -- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. -- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -- `src/provider`: Contains [module providers](#create-module-providers). -- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. -- `package.json`: Contains the plugin's package information, including general information and dependencies. -- `tsconfig.json`: Contains the TypeScript configuration for the plugin. - -*** - -## 2. Prepare Plugin - -### Package Name - -Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. - -For example: - -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` - -### Package Keywords - -Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. - -Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. - -```json title="package.json" -{ - "keywords": [ - "medusa-plugin-integration", - "medusa-v2" - ], - // ... -} -``` - -In addition, make sure to use one of the following keywords based on your integration type: - -|Keyword|Description|Example| -|---|---|---| -|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| -|\`medusa-plugin-auth\`|Authentication service integration|Auth0| -|\`medusa-plugin-cms\`|CMS service integration|Contentful| -|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| -|\`medusa-plugin-payment\`|Payment service integration|PayPal| -|\`medusa-plugin-search\`|Search service integration|MeiliSearch| -|\`medusa-plugin-shipping\`|Shipping service integration|DHL| -|\`medusa-plugin-other\`|Other third-party integrations|Sentry| - -### Package Dependencies - -Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. - -In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. - -For example, assuming `2.5.0` is the latest Medusa version: - -```json title="package.json" -{ - "devDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/ui": "4.0.4", - "@medusajs/icons": "2.5.0", - "@swc/core": "1.5.7", - }, - "peerDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/ui": "4.0.3", - "@medusajs/icons": "2.5.0", - } -} -``` - -### Package Exports - -Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. - -In the `package.json` file, make sure your plugin has the following exports: - -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./admin": { - "import": "./.medusa/server/src/admin/index.mjs", - "require": "./.medusa/server/src/admin/index.js", - "default": "./.medusa/server/src/admin/index.js" - }, - "./*": "./.medusa/server/src/*.js" - } -} -``` - -Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. - -The plugin exports the following files and directories: - -- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. -- `./workflows`: The workflows exported in `./src/workflows/index.ts`. -- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. -- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. -- `./admin`: The admin extensions exported in `./src/admin/index.ts`. -- `./*`: Any other files in the plugin's `src` directory. - -*** - -## 3. Publish Plugin Locally for Development and Testing - -Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. - -### Publish and Install Local Package - -### Prerequisites - -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) - -The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. - -To publish the plugin to the local registry, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, navigate to your Medusa application: - -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` - -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. - -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm install --save-dev yalc -``` - -After that, run the following Medusa CLI command to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` - -Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. - -### Register Plugin in Medusa Application - -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. - -Add the plugin to the `plugins` array in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], -}) -``` - -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. - -#### Pass Module Options through Plugin - -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. - -For example: - -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], -}) -``` - -The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - -### Watch Plugin Changes During Development - -While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. - -To do that, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -This command will: - -- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. -- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. - -### Start Medusa Application - -You can start your Medusa application's development server to test out your plugin: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm run dev -``` - -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. - -*** - -## 4. Create Customizations in the Plugin - -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. - -- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) -- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) -- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) - -While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). - -### Generating Migrations for Modules - -During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: - -```plain title="Plugin project" -DB_USERNAME=postgres -DB_PASSWORD=123... -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=db_name -``` - -You can add these environment variables in a `.env` file in your plugin project. The variables are: - -- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. -- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. -- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. -- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. -- `DB_NAME`: The name of the PostgreSQL database to connect to. - -Then, run the following command in your plugin project to generate migrations for the modules in your plugin: - -```bash title="Plugin project" -npx medusa plugin:db:generate -``` - -This command generates migrations for all modules in the plugin. - -Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: - -```bash title="Medusa application" -npx medusa db:migrate -``` - -The migrations in your application, including your plugin, will run and update the database. - -### Importing Module Resources - -In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. - -These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. - -For example, to import the plugin's workflow in your Medusa application: - -`@myorg/plugin-name` is the plugin package's name. - -```ts -import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" -import BlogModule from "@myorg/plugin-name/modules/blog" -// import other files created in plugin like ./src/types/blog.ts -import BlogType from "@myorg/plugin-name/types/blog" -``` - -### Create Module Providers - -The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. - -For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: - -`@myorg/plugin-name` is the plugin package's name. - -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@myorg/plugin-name/providers/my-notification", - id: "my-notification", - options: { - channels: ["email"], - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. - -To learn how to create module providers, refer to the following guides: - -- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) -- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) -- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) -- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) -- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) -- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) - -*** - -## 5. Publish Plugin to NPM - -Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. - -Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: - -```bash -npx medusa plugin:build -``` - -The command will compile an output in the `.medusa/server` directory. - -You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: - -```bash -npm publish -``` - -If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. - -### Install Public Plugin in Medusa Application - -You install a plugin that's published publicly using your package manager. For example: - -```bash npm2yarn -npm install @myorg/plugin-name -``` - -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. - -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). - -*** - -## Update a Published Plugin - -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). - -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. - -First, run the following command to change the version of the plugin: - -```bash -npm version -``` - -Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. - -Then, re-run the same commands for publishing a plugin: - -```bash -npx medusa plugin:build -npm publish -``` - -This will publish an updated version of your plugin under a new version. - - # Compensation Function In this chapter, you'll learn what a compensation function is and how to add it to a step. @@ -15546,6 +14732,721 @@ So, if an error occurs during the loop, the compensation function will still rec For more details on error handling in workflows and steps, check the [Handling Errors chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). +# Query + +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. + +## What is Query? + +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. + +In all resources that can access the [Medusa Container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md), such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s Commerce Modules. + +*** + +## Query Example + +For example, create the route `src/api/query/route.ts` with the following content: + +```ts title="src/api/query/route.ts" highlights={exampleHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + }) + + res.json({ posts }) +} +``` + +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. + +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: + +- `entity`: The data model's name, as specified in the first parameter of the `model.define` method used for the data model's definition. +- `fields`: An array of the data model’s properties to retrieve in the result. + +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: + +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "title": "My Post" + } + ] +} +``` + +*** + +## Querying the Graph + +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. + +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. + +*** + +## Retrieve Linked Records + +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. + +For example: + +```ts highlights={[["6"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: [ + "id", + "title", + "product.*", + ], +}) +``` + +`.*` means that all of data model's properties should be retrieved. You can also retrieve specific properties by replacing the `*` with the property name, for each property. + +For example: + +```ts +const { data: posts } = await query.graph({ + entity: "post", + fields: [ + "id", + "title", + "product.id", + "product.title", + ], +}) +``` + +In the example above, you retrieve only the `id` and `title` properties of the `product` linked to a `post`. + +### Retrieve List Link Records + +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. + +For example: + +```ts highlights={[["6"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: [ + "id", + "title", + "products.*", + ], +}) +``` + +In the example above, you retrieve all products linked to a post. + +### Apply Filters and Pagination on Linked Records + +Consider you want to apply filters or pagination configurations on the product(s) linked to `post`. To do that, you must query the module link's table instead. + +As mentioned in the [Module Link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) documentation, Medusa creates a table for your module link. So, not only can you retrieve linked records, but you can also retrieve the records in a module link's table. + +A module link's definition, exported by a file under `src/links`, has a special `entryPoint` property. Use this property when specifying the `entity` property in Query's `graph` method. + +For example: + +```ts highlights={queryLinkTableHighlights} +import ProductPostLink from "../../../links/product-post" + +// ... + +const { data: productCustoms } = await query.graph({ + entity: ProductPostLink.entryPoint, + fields: ["*", "product.*", "post.*"], + pagination: { + take: 5, + skip: 0, + }, +}) +``` + +In the object passed to the `graph` method: + +- You pass the `entryPoint` property of the link definition as the value for `entity`. So, Query will retrieve records from the module link's table. +- You pass three items to the `field` property: + - `*` to retrieve the link table's fields. This is useful if the link table has [custom columns](https://docs.medusajs.com/learn/fundamentals/module-links/custom-columns/index.html.md). + - `product.*` to retrieve the fields of a product record linked to a `Post` record. + - `post.*` to retrieve the fields of a `Post` record linked to a product record. + +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination) on the module link's table. For example, you can apply filters on the `product_id`, `post_id`, and any other custom columns you defined in the link table. + +The returned `data` is similar to the following: + +```json title="Example Result" +[{ + "id": "123", + "product_id": "prod_123", + "post_id": "123", + "product": { + "id": "prod_123", + // other product fields... + }, + "post": { + "id": "123", + // other post fields... + } +}] +``` + +*** + +## Apply Filters + +```ts highlights={[["4"], ["5"], ["6"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + id: "post_123", + }, +}) +``` + +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. + +In the example above, you filter the `post` records by the ID `post_123`. + +You can also filter by multiple values of a property. For example: + +```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"], ["9"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + id: [ + "post_123", + "post_321", + ], + }, +}) +``` + +In the example above, you filter the `post` records by multiple IDs. + +Filters don't apply on fields of linked data models from other modules. Refer to the [Retrieve Linked Records](#retrieve-linked-records) section for an alternative solution. + +### Advanced Query Filters + +Under the hood, Query uses one of the following methods from the data model's module's service to retrieve records: + +- `listX` if you don't pass [pagination parameters](#apply-pagination). For example, `listPosts`. +- `listAndCountX` if you pass pagination parameters. For example, `listAndCountPosts`. + +Both methods accepts a filter object that can be used to filter records. + +Those filters don't just allow you to filter by exact values. You can also filter by properties that don't match a value, match multiple values, and other filter types. + +Refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/tips/filtering/index.html.md) for examples of advanced filters. The following sections provide some quick examples. + +#### Filter by Not Matching a Value + +```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + title: { + $ne: null, + }, + }, +}) +``` + +In the example above, only posts that have a title are retrieved. + +#### Filter by Not Matching Multiple Values + +```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + title: { + $nin: ["My Post", "Another Post"], + }, + }, +}) +``` + +In the example above, only posts that don't have the title `My Post` or `Another Post` are retrieved. + +#### Filter by a Range + +```ts highlights={[["10"], ["11"], ["12"], ["13"], ["14"], ["15"]]} +const startToday = new Date() +startToday.setHours(0, 0, 0, 0) + +const endToday = new Date() +endToday.setHours(23, 59, 59, 59) + +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + published_at: { + $gt: startToday, + $lt: endToday, + }, + }, +}) +``` + +In the example above, only posts that were published today are retrieved. + +#### Filter Text by Like Value + +This filter only applies to text-like properties, including `text`, `id`, and `enum` properties. + +```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + title: { + $like: "%My%", + }, + }, +}) +``` + +In the example above, only posts that have the word `My` in their title are retrieved. + +#### Filter a Relation's Property + +```ts highlights={[["4"], ["5"], ["6"], ["7"], ["8"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + author: { + name: "John", + }, + }, +}) +``` + +While it's not possible to filter by a linked data model's property, you can filter by a relation's property (that is, the property of a related data model that is defined in the same module). + +In the example above, only posts that have an author with the name `John` are retrieved. + +*** + +## Apply Pagination + +```ts highlights={[["8", "skip", "The number of records to skip before fetching the results."], ["9", "take", "The number of records to fetch."]]} +const { + data: posts, + metadata: { count, take, skip } = {}, +} = await query.graph({ + entity: "post", + fields: ["id", "title"], + pagination: { + skip: 0, + take: 10, + }, +}) +``` + +The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. + +To paginate the returned records, pass the following properties to `pagination`: + +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. + +When you provide the pagination fields, the `query.graph` method's returned object has a `metadata` property. Its value is an object having the following properties: + +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. + +### Sort Records + +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + pagination: { + order: { + name: "DESC", + }, + }, +}) +``` + +Sorting doesn't work on fields of linked data models from other modules. + +To sort returned records, pass an `order` property to `pagination`. + +The `order` property is an object whose keys are property names, and values are either: + +- `ASC` to sort records by that property in ascending order. +- `DESC` to sort records by that property in descending order. + +*** + +## Configure Query to Throw Errors + +By default, if Query doesn't find records matching your query, it returns an empty array. You can add option to configure Query to throw an error when no records are found. + +The `query.graph` method accepts as a second parameter an object that can have a `throwIfKeyNotFound` property. Its value is a boolean indicating whether to throw an error if no record is found when filtering by IDs. By default, it's `false`. + +For example: + +```ts +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title"], + filters: { + id: "post_123", + }, +}, { + throwIfKeyNotFound: true, +}) +``` + +In the example above, if no post is found with the ID `post_123`, Query will throw an error. This is useful to stop execution when a record is expected to exist. + +### Throw Error on Related Data Model + +The `throwIfKeyNotFound` option can also be used to throw an error if the ID of a related data model's record (in the same module) is passed in the filters, and the related record doesn't exist. + +For example: + +```ts +const { data: posts } = await query.graph({ + entity: "post", + fields: ["id", "title", "author.*"], + filters: { + id: "post_123", + author_id: "author_123", + }, +}, { + throwIfKeyNotFound: true, +}) +``` + +In the example above, Query throws an error either if no post is found with the ID `post_123` or if its found but its author ID isn't `author_123`. + +In the above example, it's assumed that a post belongs to an author, so it has an `author_id` property. However, this also works in the opposite case, where an author has many posts. + +For example: + +```ts +const { data: posts } = await query.graph({ + entity: "author", + fields: ["id", "name", "posts.*"], + filters: { + id: "author_123", + posts: { + id: "post_123", + }, + }, +}, { + throwIfKeyNotFound: true, +}) +``` + +In the example above, Query throws an error if no author is found with the ID `author_123` or if the author is found but doesn't have a post with the ID `post_123`. + +*** + +## Request Query Configurations + +For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: + +- Validates accepted query parameters, as explained in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). +- Parses configurations that are received as query parameters to be passed to Query. + +Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. + +### Step 1: Add Middleware + +The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: + +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" + +export const GetCustomSchema = createFindParams() + +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "title", + "products.*", + ], + isList: true, + } + ), + ], + }, + ], +}) +``` + +The `validateAndTransformQuery` accepts two parameters: + +1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: + 1. `fields`: The fields and relations to retrieve in the returned resources. + 2. `offset`: The number of items to skip before retrieving the returned items. + 3. `limit`: The maximum number of items to return. + 4. `order`: The fields to order the returned items by in ascending or descending order. +2. A Query configuration object. It accepts the following properties: + 1. `defaults`: An array of default fields and relations to retrieve in each resource. + 2. `isList`: A boolean indicating whether a list of items are returned in the response. + 3. `allowed`: An array of fields and relations allowed to be passed in the `fields` query parameter. + 4. `defaultLimit`: A number indicating the default limit to use if no limit is provided. By default, it's `50`. + +### Step 2: Use Configurations in API Route + +After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. + +The middleware transforms these parameters to configurations that you can pass to Query in your API route handler. These configurations are stored in the `queryConfig` parameter of the `MedusaRequest` object. + +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been deprecated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. + +For example, Create the file `src/api/customs/route.ts` with the following content: + +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const { data: posts } = await query.graph({ + entity: "post", + ...req.queryConfig, + }) + + res.json({ posts: posts }) +} +``` + +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. + +In the API route, you pass `req.queryConfig` to `query.graph`. `queryConfig` has properties like `fields` and `pagination` to configure the query based on the default values you specified in the middleware, and the query parameters passed in the request. + +### Test it Out + +To test it out, start your Medusa application and send a `GET` request to the `/customs` API route. A list of records are retrieved with the specified fields in the middleware. + +```json title="Returned Data" +{ + "posts": [ + { + "id": "123", + "title": "test" + } + ] +} +``` + +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. + +Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. + + +# Conditions in Workflows with When-Then + +In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. + +## Why If-Conditions Aren't Allowed in Workflows? + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. + +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. + +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. + +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. + +*** + +## How to use When-Then? + +The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. + +For example: + +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... + +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) + + // executed without condition + const anotherStepResult = anotherStep(result) + + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` + +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. + +### When Parameters + +`when` accepts the following parameters: + +1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. + +### Then Parameters + +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. + +The callback function is only executed if `when`'s second parameter function returns a `true` value. + +*** + +## Implementing If-Else with When-Then + +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. + +For example: + +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) + + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) + + // ... + } +) +``` + +In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. + +*** + +## Specify Name for When-Then + +Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: + +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` + +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. + +However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. + +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: + +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active + } +).then(() => { + const isActive = isActiveStep() + + return { + isActive, + } +}) +``` + +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: + +1. A unique name to be assigned to the `when-then` block. +2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +3. A function that returns a boolean indicating whether to execute the action in `then`. + +The second and third parameters are the same as the parameters you previously passed to `when`. + + # Error Handling in Workflows In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions. @@ -16145,6 +16046,205 @@ const step1 = createStep( ``` +# Execute Another Workflow + +In this chapter, you'll learn how to execute a workflow in another. + +## Execute in a Workflow + +To execute a workflow in another, use the `runAsStep` method that every workflow has. + +For example: + +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) + + // ... + } +) +``` + +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. + +*** + +## Preparing Input Data + +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. + +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +For example: + +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + title: string +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) + + // ... + } +) +``` + +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. + +*** + +## Run Workflow Conditionally + +To run a workflow in another based on a condition, use when-then from the Workflows SDK. + +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). + +For example: + +```ts highlights={whenHighlights} collapsibleLines="1-16" +import { + createWorkflow, + when, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" +import { + CreateProductWorkflowInputDTO, +} from "@medusajs/framework/types" + +type WorkflowInput = { + product?: CreateProductWorkflowInputDTO + should_create?: boolean +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const product = when(input, ({ should_create }) => should_create) + .then(() => { + return createProductsWorkflow.runAsStep({ + input: { + products: [input.product], + }, + }) + }) + } +) +``` + +In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + + +# Expose a Workflow Hook + +In this chapter, you'll learn how to expose a hook in your workflow. + +## When to Expose a Hook + +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. + +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. + +*** + +## How to Expose a Hook in a Workflow? + +To expose a hook in your workflow, use `createHook` from the Workflows SDK. + +For example: + +```ts title="src/workflows/my-workflow/index.ts" highlights={hookHighlights} +import { + createStep, + createHook, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { createProductStep } from "./steps/create-product" + +export const myWorkflow = createWorkflow( + "my-workflow", + function (input) { + const product = createProductStep(input) + const productCreatedHook = createHook( + "productCreated", + { productId: product.id } + ) + + return new WorkflowResponse(product, { + hooks: [productCreatedHook], + }) + } +) +``` + +The `createHook` function accepts two parameters: + +1. The first is a string indicating the hook's name. You use this to consume the hook later. +2. The second is the input to pass to the hook handler. + +The workflow must also pass an object having a `hooks` property as a second parameter to the `WorkflowResponse` constructor. Its value is an array of the workflow's hooks. + +### How to Consume the Hook? + +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: + +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" + +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + } +) +``` + +The hook is available on the workflow's `hooks` property using its name `productCreated`. + +You invoke the hook, passing a step function (the hook handler) as a parameter. + + # Long-Running Workflows In this chapter, you’ll learn what a long-running workflow is and how to configure it. @@ -16440,136 +16540,6 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions. -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. - -## Execute in a Workflow - -To execute a workflow in another, use the `runAsStep` method that every workflow has. - -For example: - -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) - - // ... - } -) -``` - -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. - -The object has an `input` property to pass input to the workflow. - -*** - -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. - -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -For example: - -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -type WorkflowInput = { - title: string -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - - // ... - } -) -``` - -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. - -*** - -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. - -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -For example: - -```ts highlights={whenHighlights} collapsibleLines="1-16" -import { - createWorkflow, - when, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" -import { - CreateProductWorkflowInputDTO, -} from "@medusajs/framework/types" - -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) -``` - -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. - - # Run Workflow Steps in Parallel In this chapter, you’ll learn how to run workflow steps in parallel. @@ -16697,129 +16667,6 @@ The `config` method accepts an object with a `name` property. Its value is a new The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. -# Retry Failed Steps - -In this chapter, you’ll learn how to configure steps to allow retrial on failure. - -## What is a Step Retrial? - -A step retrial is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service. - -When a step fails, the workflow engine can automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows. - -You can also configure the interval between retries, allowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time. - -For example, if a step captures a payment, you may want to retry it the next day until the payment is successful or the maximum number of retries is reached. - -*** - -## Configure a Step’s Retrial - -By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. - -You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - }, - async () => { - console.log("Executing step 1") - - throw new Error("Oops! Something happened.") - } -) - -const myWorkflow = createWorkflow( - "hello-world", - function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow -``` - -The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails. - -When you execute the above workflow, you’ll see the following result in the terminal: - -```bash -Executing step 1 -Executing step 1 -Executing step 1 -error: Oops! Something happened. -Error: Oops! Something happened. -``` - -The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail. - -*** - -## Step Retry Intervals - -By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -In this example, if the step fails, it will be retried after two seconds. - -### Maximum Retry Interval - -The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period. - -For example, to retry a step after a day: - -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 86400, // 1 day - }, - async () => { - // ... - } -) -``` - -In this example, if the step fails, it will be retried after `86400` seconds (one day). - -### Interval Changes Workflow to Long-Running - -By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered. - -However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow. - -Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). - - # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -16965,216 +16812,6 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - -*** - -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. - -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - // ... - } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. - -*** - -## Configure Step Timeout - -Alternatively, you can configure the timeout for a step rather than the entire workflow. - -As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -This step's executions fail if they run longer than two seconds. - -A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. - - -# Workflow Hooks - -In this chapter, you'll learn what a workflow hook is and how to consume them. - -## What is a Workflow Hook? - -A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. - -Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. - -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. - -You want to perform a custom action during a workflow's execution, such as when a product is created. - -*** - -## How to Consume a Hook? - -A workflow has a special `hooks` property which is an object that holds its hooks. - -So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: - -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. - -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: - -```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - } -) -``` - -The `productsCreated` hook is available on the workflow's `hooks` property by its name. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - -Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. - -A hook can have only one handler. - -Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. - -### Hook Handler Parameter - -Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. - -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. - -### Hook Handler Compensation - -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. - -For example: - -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - - return new StepResponse(undefined, { ids }) - }, - async ({ ids }, { container }) => { - // undo the performed action - } -) -``` - -The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. - -The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. - -It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - -### Additional Data Property - -Medusa's workflows pass in the hook's input an `additional_data` property: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action - } -) -``` - -This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. - -Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). - -### Pass Additional Data to Workflow - -You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} -import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", - }, - }, - }) -} -``` - -Your hook handler then receives that passed data in the `additional_data` object. - - # Data Manipulation in Workflows with transform In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate data and variables in a workflow. @@ -17380,6 +17017,369 @@ const myWorkflow = createWorkflow( ``` +# Scheduled Jobs Number of Executions + +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. + +## numberOfExecutions Option + +The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. + +For example: + +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} + +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` + +The above scheduled job has the `numberOfExecutions` configuration set to `3`. + +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. + +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. + + +# Workflow Hooks + +In this chapter, you'll learn what a workflow hook is and how to consume them. + +## What is a Workflow Hook? + +A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. + +Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. + +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. + +You want to perform a custom action during a workflow's execution, such as when a product is created. + +*** + +## How to Consume a Hook? + +A workflow has a special `hooks` property which is an object that holds its hooks. + +So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: + +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. + +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: + +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + } +) +``` + +The `productsCreated` hook is available on the workflow's `hooks` property by its name. + +You invoke the hook, passing a step function (the hook handler) as a parameter. + +Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. + +A hook can have only one handler. + +Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. + +### Hook Handler Parameter + +Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. + +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. + +### Hook Handler Compensation + +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. + +For example: + +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + + return new StepResponse(undefined, { ids }) + }, + async ({ ids }, { container }) => { + // undo the performed action + } +) +``` + +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. + +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. + +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. + +### Additional Data Property + +Medusa's workflows pass in the hook's input an `additional_data` property: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action + } +) +``` + +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. + +Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). + +### Pass Additional Data to Workflow + +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +export async function POST(req: MedusaRequest, res: MedusaResponse) { + await createProductsWorkflow(req.scope).run({ + input: { + products: [ + // ... + ], + additional_data: { + custom_field: "test", + }, + }, + }) +} +``` + +Your hook handler then receives that passed data in the `additional_data` object. + + +# Workflow Timeout + +In this chapter, you’ll learn how to set a timeout for workflows and steps. + +## What is a Workflow Timeout? + +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. + +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. + +### Timeout Doesn't Stop Step Execution + +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. + +*** + +## Configure Workflow Timeout + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. + +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow + +``` + +This workflow's executions fail if they run longer than two seconds. + +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. + +*** + +## Configure Step Timeout + +Alternatively, you can configure the timeout for a step rather than the entire workflow. + +As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. + +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. + +For example: + +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +This step's executions fail if they run longer than two seconds. + +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. + + +# Retry Failed Steps + +In this chapter, you’ll learn how to configure steps to allow retrial on failure. + +## What is a Step Retrial? + +A step retrial is a mechanism that allows a step to be retried automatically when it fails. This is useful for handling transient errors, such as network issues or temporary unavailability of a service. + +When a step fails, the workflow engine can automatically retry the step a specified number of times before marking the workflow as failed. This can help improve the reliability and resilience of your workflows. + +You can also configure the interval between retries, allowing you to wait for a certain period before attempting the step again. This is useful when the failure is due to a temporary issue that may resolve itself after some time. + +For example, if a step captures a payment, you may want to retry it the next day until the payment is successful or the maximum number of retries is reached. + +*** + +## Configure a Step’s Retrial + +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. + +You can configure the step to retry on failure. The `createStep` function can accept a configuration object instead of the step’s name as a first parameter. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") + + throw new Error("Oops! Something happened.") + } +) + +const myWorkflow = createWorkflow( + "hello-world", + function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow +``` + +The step’s configuration object accepts a `maxRetries` property, which is a number indicating the number of times a step can be retried when it fails. + +When you execute the above workflow, you’ll see the following result in the terminal: + +```bash +Executing step 1 +Executing step 1 +Executing step 1 +error: Oops! Something happened. +Error: Oops! Something happened. +``` + +The first line indicates the first time the step was executed, and the next two lines indicate the times the step was retried. After that, the step and workflow fail. + +*** + +## Step Retry Intervals + +By default, a step is retried immediately after it fails. To specify a wait time before a step is retried, pass a `retryInterval` property to the step's configuration object. Its value is a number of seconds to wait before retrying the step. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +In this example, if the step fails, it will be retried after two seconds. + +### Maximum Retry Interval + +The `retryInterval` property's maximum value is [Number.MAX\_SAFE\_INTEGER](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER). So, you can set a very long wait time before the step is retried, allowing you to retry steps after a long period. + +For example, to retry a step after a day: + +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 86400, // 1 day + }, + async () => { + // ... + } +) +``` + +In this example, if the step fails, it will be retried after `86400` seconds (one day). + +### Interval Changes Workflow to Long-Running + +By setting `retryInterval` on a step, a workflow that uses that step becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. This is useful when creating workflows that may fail and should run for a long time until they succeed, such as waiting for a payment to be captured or a shipment to be delivered. + +However, since the long-running workflow runs in the background, you won't receive its result or errors immediately when you execute the workflow. + +Instead, you must subscribe to the workflow's execution using the Workflow Engine Module Service. Learn more about it in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result/index.html.md). + + # Docs Contribution Guidelines Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. @@ -17869,6 +17869,76 @@ The `errors` property contains an array of errors thrown during the execution of If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. +# Example: Integration Tests for a Module + +In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. + +### Prerequisites + +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) + +## Write Integration Test for Module + +Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: + +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" + +class BlogModuleService extends MedusaService({ + MyCustom, +}){ + getMessage(): string { + return "Hello, World!" + } +} + +export default BlogModuleService +``` + +To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: + +```ts title="src/modules/blog/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import MyCustom from "../models/my-custom" + +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/blog", + testSuite: ({ service }) => { + describe("BlogModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. + +*** + +## Run Test + +Run the following command to run your module integration tests: + +```bash npm2yarn +npm run test:integration:modules +``` + +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). + +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. + + # Example: Write Integration Tests for API Routes In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. @@ -18438,76 +18508,6 @@ const response = await api.post(`/custom`, form, { ``` -# Example: Integration Tests for a Module - -In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## Write Integration Test for Module - -Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: - -```ts title="src/modules/blog/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" - -class BlogModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - -To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: - -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import MyCustom from "../models/my-custom" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - describe("BlogModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() - - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. - -*** - -## Run Test - -Run the following command to run your module integration tests: - -```bash npm2yarn -npm run test:integration:modules -``` - -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. - - # Commerce Modules In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules. @@ -18829,434 +18829,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Cart Module - -In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. - -Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Cart Features - -- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. -- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. - -*** - -## How to Use the Cart Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-cart.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCartStep = createStep( - "create-cart", - async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) - - const cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - address_1: "1512 Barataria Blvd", - country_code: "us", - }, - items: [ - { - title: "Shirt", - unit_price: 1000, - quantity: 1, - }, - ], - }) - - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) - - await cartModuleService.deleteCarts([cartId]) - } -) - -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() - - return new WorkflowResponse({ - cart, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createCartWorkflow } from "../../workflows/create-cart" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Currency Module - -In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. - -Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Currency Features - -- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. -- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. - -*** - -## How to Use the Currency Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) - - const currency = await currencyModuleService - .retrieveCurrency("usd") - - return new StepResponse({ currency }) - } -) - -type Input = { - price: number -} - -export const retrievePriceWithCurrency = createWorkflow( - "create-currency", - (input: Input) => { - const { currency } = retrieveCurrencyStep() - - const formattedPrice = transform({ - input, - currency, - }, (data) => { - return `${data.currency.symbol}${data.input.price}` - }) - - return new WorkflowResponse({ - formattedPrice, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Auth Module - -In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. - -Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Auth Features - -- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. -- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). -- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. -- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. - -*** - -## How to Use the Auth Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/authenticate-user.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules, MedusaError } from "@medusajs/framework/utils" -import { MedusaRequest } from "@medusajs/framework/http" -import { AuthenticationInput } from "@medusajs/framework/types" - -type Input = { - req: MedusaRequest -} - -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) - - const { success, authIdentity, error } = await authModuleService - .authenticate( - "emailpass", - { - url: req.url, - headers: req.headers, - query: req.query, - body: req.body, - authScope: "admin", // or custom actor type - protocol: req.protocol, - } as AuthenticationInput - ) - - if (!success) { - // incorrect authentication details - throw new MedusaError( - MedusaError.Types.UNAUTHORIZED, - error || "Incorrect authentication details" - ) - } - - return new StepResponse({ authIdentity }, authIdentity?.id) - }, - async (authIdentityId, { container }) => { - if (!authIdentityId) { - return - } - - const authModuleService = container.resolve(Modules.AUTH) - - await authModuleService.deleteAuthIdentities([authIdentityId]) - } -) - -export const authenticateUserWorkflow = createWorkflow( - "authenticate-user", - (input: Input) => { - const { authIdentity } = authenticateUserStep(input) - - return new WorkflowResponse({ - authIdentity, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { authenticateUserWorkflow } from "../../workflows/authenticate-user" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) - - res.send(result) -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Auth Module - -The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. - -*** - -## Providers - -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - -*** - - # Fulfillment Module In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application. @@ -19424,6 +18996,154 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** +# Currency Module + +In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. + +Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Currency Features + +- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. +- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. + +*** + +## How to Use the Currency Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const retrieveCurrencyStep = createStep( + "retrieve-currency", + async ({}, { container }) => { + const currencyModuleService = container.resolve(Modules.CURRENCY) + + const currency = await currencyModuleService + .retrieveCurrency("usd") + + return new StepResponse({ currency }) + } +) + +type Input = { + price: number +} + +export const retrievePriceWithCurrency = createWorkflow( + "create-currency", + (input: Input) => { + const { currency } = retrieveCurrencyStep() + + const formattedPrice = transform({ + input, + currency, + }, (data) => { + return `${data.currency.symbol}${data.input.price}` + }) + + return new WorkflowResponse({ + formattedPrice, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Inventory Module In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. @@ -19568,27 +19288,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Pricing Module +# Cart Module -In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. - -Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. +Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Pricing Features +## Cart Features -- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. -- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. -- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. -- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. -- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. +- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. +- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. *** -## How to Use the Pricing Module +## How to Use the Cart Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -19596,7 +19313,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-price-set.ts" highlights={highlights} +```ts title="src/workflows/create-cart.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -19605,46 +19322,45 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPriceSetStep = createStep( - "create-price-set", +const createCartStep = createStep( + "create-cart", async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) + const cartModuleService = container.resolve(Modules.CART) - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + shipping_address: { + address_1: "1512 Barataria Blvd", + country_code: "us", + }, + items: [ { - amount: 500, - currency_code: "USD", - }, - { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, + title: "Shirt", + unit_price: 1000, + quantity: 1, }, ], }) - return new StepResponse({ priceSet }, priceSet.id) + return new StepResponse({ cart }, cart.id) }, - async (priceSetId, { container }) => { - if (!priceSetId) { + async (cartId, { container }) => { + if (!cartId) { return } - const pricingModuleService = container.resolve(Modules.PRICING) + const cartModuleService = container.resolve(Modules.CART) - await pricingModuleService.deletePriceSets([priceSetId]) + await cartModuleService.deleteCarts([cartId]) } ) -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", +export const createCartWorkflow = createWorkflow( + "create-cart", () => { - const { priceSet } = createPriceSetStep() + const { cart } = createCartStep() return new WorkflowResponse({ - priceSet, + cart, }) } ) @@ -19659,13 +19375,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPriceSetWorkflow } from "../../workflows/create-price-set" +import { createCartWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPriceSetWorkflow(req.scope) + const { result } = await createCartWorkflow(req.scope) .run() res.send(result) @@ -19679,13 +19395,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createCartWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -19700,12 +19416,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createCartWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -19722,6 +19438,136 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Auth Module + +In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. + +Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Auth Features + +- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. +- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). +- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. +- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. + +*** + +## How to Use the Auth Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" + +type Input = { + req: MedusaRequest +} + +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) + + const { success, authIdentity, error } = await authModuleService + .authenticate( + "emailpass", + { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: "admin", // or custom actor type + protocol: req.protocol, + } as AuthenticationInput + ) + + if (!success) { + // incorrect authentication details + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Incorrect authentication details" + ) + } + + return new StepResponse({ authIdentity }, authIdentity?.id) + }, + async (authIdentityId, { container }) => { + if (!authIdentityId) { + return + } + + const authModuleService = container.resolve(Modules.AUTH) + + await authModuleService.deleteAuthIdentities([authIdentityId]) + } +) + +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) + + return new WorkflowResponse({ + authIdentity, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) + + res.send(result) +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure Auth Module + +The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. + +*** + +## Providers + +Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. + +*** + + # Payment Module In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. @@ -19877,27 +19723,26 @@ Medusa provides the following payment providers out-of-the-box. You can use them *** -# Order Module +# Promotion Module -In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. -Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. +Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Order Features +## Promotion Features -- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. -- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. -- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. +- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. +- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. +- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. +- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. *** -## How to Use the Order Module +## How to Use the Promotion Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -19905,7 +19750,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +```ts title="src/workflows/create-promotion.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -19914,48 +19759,41 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createDraftOrderStep = createStep( - "create-order", +const createPromotionStep = createStep( + "create-promotion", async ({}, { container }) => { - const orderModuleService = container.resolve(Modules.ORDER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - const draftOrder = await orderModuleService.createOrders({ - currency_code: "usd", - items: [ - { - title: "Shirt", - quantity: 1, - unit_price: 3000, - }, - ], - shipping_methods: [ - { - name: "Express shipping", - amount: 3000, - }, - ], - status: "draft", + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, + currency_code: "usd", + }, }) - return new StepResponse({ draftOrder }, draftOrder.id) + return new StepResponse({ promotion }, promotion.id) }, - async (draftOrderId, { container }) => { - if (!draftOrderId) { + async (promotionId, { container }) => { + if (!promotionId) { return } - const orderModuleService = container.resolve(Modules.ORDER) + const promotionModuleService = container.resolve(Modules.PROMOTION) - await orderModuleService.deleteOrders([draftOrderId]) + await promotionModuleService.deletePromotions(promotionId) } ) -export const createDraftOrderWorkflow = createWorkflow( - "create-draft-order", +export const createPromotionWorkflow = createWorkflow( + "create-promotion", () => { - const { draftOrder } = createDraftOrderStep() + const { promotion } = createPromotionStep() return new WorkflowResponse({ - draftOrder, + promotion, }) } ) @@ -19970,13 +19808,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" +import { createPromotionWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createDraftOrderWorkflow(req.scope) + const { result } = await createPromotionWorkflow(req.scope) .run() res.send(result) @@ -19990,13 +19828,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -20011,12 +19849,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +import { createPromotionWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createDraftOrderWorkflow(container) + const { result } = await createPromotionWorkflow(container) .run() console.log(result) @@ -20188,6 +20026,316 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Order Module + +In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/index.html.md) to learn how to manage orders using the dashboard. + +Medusa has order related features available out-of-the-box through the Order Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Order Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Order Features + +- [Order Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/concepts/index.html.md): Store and manage your orders to retrieve, create, cancel, and perform other operations. +- Draft Orders: Allow merchants to create orders on behalf of their customers as draft orders that later are transformed to regular orders. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/promotion-adjustments/index.html.md): Apply promotions or discounts to the order's items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/tax-lines/index.html.md): Apply tax lines to an order's line items and shipping methods. +- [Returns](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md), [Edits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md), [Exchanges](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md), and [Claims](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md): Make [changes](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-change/index.html.md) to an order to edit, return, or exchange its items, with [version-based control](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/order-versioning/index.html.md) over the order's timeline. + +*** + +## How to Use the Order Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-draft-order.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createDraftOrderStep = createStep( + "create-order", + async ({}, { container }) => { + const orderModuleService = container.resolve(Modules.ORDER) + + const draftOrder = await orderModuleService.createOrders({ + currency_code: "usd", + items: [ + { + title: "Shirt", + quantity: 1, + unit_price: 3000, + }, + ], + shipping_methods: [ + { + name: "Express shipping", + amount: 3000, + }, + ], + status: "draft", + }) + + return new StepResponse({ draftOrder }, draftOrder.id) + }, + async (draftOrderId, { container }) => { + if (!draftOrderId) { + return + } + const orderModuleService = container.resolve(Modules.ORDER) + + await orderModuleService.deleteOrders([draftOrderId]) + } +) + +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", + () => { + const { draftOrder } = createDraftOrderStep() + + return new WorkflowResponse({ + draftOrder, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createDraftOrderWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createDraftOrderWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + +# Pricing Module + +In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. + +Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Pricing Features + +- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. +- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. +- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. +- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. +- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. + +*** + +## How to Use the Pricing Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-price-set.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createPriceSetStep = createStep( + "create-price-set", + async ({}, { container }) => { + const pricingModuleService = container.resolve(Modules.PRICING) + + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + { + amount: 500, + currency_code: "USD", + }, + { + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, + }, + ], + }) + + return new StepResponse({ priceSet }, priceSet.id) + }, + async (priceSetId, { container }) => { + if (!priceSetId) { + return + } + const pricingModuleService = container.resolve(Modules.PRICING) + + await pricingModuleService.deletePriceSets([priceSetId]) + } +) + +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", + () => { + const { priceSet } = createPriceSetStep() + + return new WorkflowResponse({ + priceSet, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPriceSetWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createPriceSetWorkflow } from "../workflows/create-price-set" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPriceSetWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createPriceSetWorkflow } from "../workflows/create-price-set" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPriceSetWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Region Module In this section of the documentation, you will find resources to learn more about the Region Module and how to use it in your application. @@ -20331,26 +20479,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Promotion Module +# Store Module -In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. -Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. +Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Promotion Features +## Store Features -- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. -- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. -- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. -- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. +- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. +- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. *** -## How to Use the Promotion Module +## How to Use Store Module's Service In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -20358,7 +20504,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-promotion.ts" highlights={highlights} +```ts title="src/workflows/create-store.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -20367,42 +20513,37 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPromotionStep = createStep( - "create-promotion", +const createStoreStep = createStep( + "create-store", async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) + const storeModuleService = container.resolve(Modules.STORE) - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, + const store = await storeModuleService.createStores({ + name: "My Store", + supported_currencies: [{ currency_code: "usd", - }, + is_default: true, + }], }) - return new StepResponse({ promotion }, promotion.id) + return new StepResponse({ store }, store.id) }, - async (promotionId, { container }) => { - if (!promotionId) { + async (storeId, { container }) => { + if(!storeId) { return } - const promotionModuleService = container.resolve(Modules.PROMOTION) - - await promotionModuleService.deletePromotions(promotionId) + const storeModuleService = container.resolve(Modules.STORE) + + await storeModuleService.deleteStores([storeId]) } ) -export const createPromotionWorkflow = createWorkflow( - "create-promotion", +export const createStoreWorkflow = createWorkflow( + "create-store", () => { - const { promotion } = createPromotionStep() + const { store } = createStoreStep() - return new WorkflowResponse({ - promotion, - }) + return new WorkflowResponse({ store }) } ) ``` @@ -20416,13 +20557,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" +import { createStoreWorkflow } from "../../workflows/create-store" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPromotionWorkflow(req.scope) + const { result } = await createStoreWorkflow(req.scope) .run() res.send(result) @@ -20436,13 +20577,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createStoreWorkflow } from "../workflows/create-store" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createStoreWorkflow(container) .run() console.log(result) @@ -20457,12 +20598,293 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createStoreWorkflow } from "../workflows/create-store" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createStoreWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + +# Tax Module + +In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Tax Features + +- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. +- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. +- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. + +*** + +## How to Use Tax Module's Service + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createTaxRegionStep = createStep( + "create-tax-region", + async ({}, { container }) => { + const taxModuleService = container.resolve(Modules.TAX) + + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) + + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) + + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) + +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() + + return new WorkflowResponse({ taxRegion }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure Tax Module + +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. + +*** + + +# Stock Location Module + +In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. + +Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Stock Location Features + +- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). +- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. + +*** + +## How to Use Stock Location Module's Service + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-stock-location.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createStockLocationStep = createStep( + "create-stock-location", + async ({}, { container }) => { + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", + }) + + return new StepResponse({ stockLocation }, stockLocation.id) + }, + async (stockLocationId, { container }) => { + if (!stockLocationId) { + return + } + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + + await stockLocationModuleService.deleteStockLocations([stockLocationId]) + } +) + +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", + () => { + const { stockLocation } = createStockLocationStep() + + return new WorkflowResponse({ stockLocation }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createStockLocationWorkflow } from "../../workflows/create-stock-location" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStockLocationWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStockLocationWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -20639,284 +21061,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module - -In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. - -Medusa has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Stock Location Features - -- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). -- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. - -*** - -## How to Use Stock Location Module's Service - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-stock-location.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createStockLocationStep = createStep( - "create-stock-location", - async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", - }) - - return new StepResponse({ stockLocation }, stockLocation.id) - }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { - return - } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - - await stockLocationModuleService.deleteStockLocations([stockLocationId]) - } -) - -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", - () => { - const { stockLocation } = createStockLocationStep() - - return new WorkflowResponse({ stockLocation }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createStockLocationWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createStockLocationWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Store Module - -In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. - -Medusa has store related features available out-of-the-box through the Store Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Store Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Store Features - -- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. -- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. - -*** - -## How to Use Store Module's Service - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-store.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createStoreStep = createStep( - "create-store", - async ({}, { container }) => { - const storeModuleService = container.resolve(Modules.STORE) - - const store = await storeModuleService.createStores({ - name: "My Store", - supported_currencies: [{ - currency_code: "usd", - is_default: true, - }], - }) - - return new StepResponse({ store }, store.id) - }, - async (storeId, { container }) => { - if(!storeId) { - return - } - const storeModuleService = container.resolve(Modules.STORE) - - await storeModuleService.deleteStores([storeId]) - } -) - -export const createStoreWorkflow = createWorkflow( - "create-store", - () => { - const { store } = createStoreStep() - - return new WorkflowResponse({ store }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createStoreWorkflow } from "../../workflows/create-store" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createStoreWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createStoreWorkflow } from "../workflows/create-store" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createStoreWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createStoreWorkflow } from "../workflows/create-store" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createStoreWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # User Module In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. @@ -21064,149 +21208,56 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# Tax Module +# API Key Concepts -In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. +In this document, you’ll learn about the different types of API keys, their expiration and verification. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. +## API Key Types -Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module. +There are two types of API keys: -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +- `publishable`: A public key used in client applications, such as a storefront. +- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. -## Tax Features - -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. +The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). *** -## How to Use Tax Module's Service +## API Key Expiration -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) - - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) - - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) - - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) - -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() - - return new WorkflowResponse({ taxRegion }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createTaxRegionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +The associated token is no longer usable or verifiable. *** -## Configure Tax Module +## Token Verification -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. +To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. + + +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. + +## `has_account` Property + +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. *** +## Email Uniqueness + +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + # Links between API Key Module and Other Modules @@ -21218,7 +21269,7 @@ The API Key Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|| in |Stored - many-to-many|| +|ApiKey|SalesChannel|Stored - many-to-many|Learn more| *** @@ -21306,57 +21357,6 @@ createRemoteLinkStep({ ``` -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. - -## API Key Types - -There are two types of API keys: - -- `publishable`: A public key used in client applications, such as a storefront. -- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. - -The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). - -*** - -## API Key Expiration - -An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). - -The associated token is no longer usable or verifiable. - -*** - -## Token Verification - -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. - - -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. - -## `has_account` Property - -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. - -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. - -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. - -*** - -## Email Uniqueness - -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - - # Links between Customer Module and Other Modules This document showcases the module links defined between the Customer Module and other Commerce Modules. @@ -21369,9 +21369,9 @@ Read-only links are used to query data across modules, but the relations aren't |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|| in |Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Read-only - has one|| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Customer|Read-only - has one|Learn more| *** @@ -21534,6 +21534,1299 @@ const { data: orders } = useQueryGraphStep({ ``` +# Fulfillment Concepts + +In this document, you’ll learn about some basic fulfillment concepts. + +## Fulfillment Set + +A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. + +A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. + +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` + +*** + +## Service Zone + +A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. + +A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. + +![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) + +A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +*** + +## Shipping Profile + +A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. + +A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. + + +# Fulfillment Module Provider + +In this document, you’ll learn what a fulfillment module provider is. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + +## What’s a Fulfillment Module Provider? + +A fulfillment module provider handles fulfilling items, typically using a third-party integration. + +Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). + +*** + +## Configure Fulfillment Providers + +The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. + +Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). + +*** + +## How to Create a Fulfillment Provider? + +Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. + + +# Links between Fulfillment Module and Other Modules + +This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. + +## Summary + +The Fulfillment Module has the following links to other modules: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Order|Fulfillment|Stored - one-to-many|Learn more| +|Return|Fulfillment|Stored - one-to-many|Learn more| +|PriceSet|ShippingOption|Stored - many-to-one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| +|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| + +*** + +## Order Module + +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. + +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) + +### Retrieve with Query + +To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +To retrieve the return, pass `return.*` in `fields`. + +### query.graph + +```ts +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) + +// fulfillments.order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) + +// fulfillments.order +``` + +### Manage with Link + +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +*** + +## Pricing Module + +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. + +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +### Retrieve with Query + +To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: + +### query.graph + +```ts +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### Manage with Link + +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). + +### Retrieve with Query + +To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: + +### query.graph + +```ts +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### Manage with Link + +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +*** + +## Stock Location Module + +The Stock Location Module provides features to manage stock locations in a store. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: + +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. + +### query.graph + +```ts +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + + +# Fulfillment Module Options + +In this document, you'll learn about the options of the Fulfillment Module. + +## providers + +The `providers` option is an array of fulfillment module providers. + +When the Medusa application starts, these providers are registered and can be used to process fulfillments. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Item Fulfillment + +In this document, you’ll learn about the concepts of item fulfillment. + +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + +*** + +## Fulfillment Processing by a Fulfillment Provider + +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) + +*** + +## data Property + +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. + +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. + +*** + +## Fulfillment Items + +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. + +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. + +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) + +*** + +## Fulfillment Label + +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. + +*** + +## Fulfillment Status + +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: + +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. + + +# Shipping Option + +In this document, you’ll learn about shipping options and their rules. + +## What’s a Shipping Option? + +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. + +When the customer places their order, they choose a shipping option to be used to fulfill their items. + +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). + +*** + +## Service Zone Restrictions + +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. + +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. + +![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) + +Service zones can be more restrictive, such as restricting to certain cities or province codes. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) + +*** + +## Shipping Option Rules + +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. + +You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. + +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: + +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. + - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). +- `value`: One or more values. + +![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) + +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. + +![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) + +*** + +## Shipping Profile and Types + +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). + +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. + +*** + +## data Property + +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. + +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. + + +# Inventory Concepts + +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. + +## InventoryItem + +An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. + +The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. + +![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) + +### Inventory Shipping Requirement + +An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. + +When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). + +*** + +## InventoryLevel + +An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. + +It has three quantity-related properties: + +- `stocked_quantity`: The available stock quantity of an item in the associated location. +- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. + +### Associated Location + +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. + +*** + +## ReservationItem + +A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. + +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. + + +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) + +*** + +## Add to Cart + +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. + +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) + +*** + +## Order Placed + +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. + +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) + +*** + +## Order Fulfillment + +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) + +*** + +## Order Return + +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. + +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) + +### Dismissed Returned Items + +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + +# Inventory Kits + +In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. + +Refer to the following user guides to learn how to use the Medusa Admin dashboard to: + +- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). +- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). + +## What is an Inventory Kit? + +An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. + +The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. + +Using inventory kits, you can implement use cases like: + +- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. +- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. + +*** + +## Multi-Part Products + +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. + +To implement this in Medusa, you can: + +- Create inventory items for each of the different parts. +- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. + +Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. + +![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) + +### Create Multi-Part Product + +Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). + +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: + +```ts highlights={multiPartsHighlights1} +import { + createInventoryItemsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // Alternatively, you can create a stock location + const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: ["*"], + filters: { + name: "European Warehouse", + }, + }) + + const inventoryItems = createInventoryItemsWorkflow.runAsStep({ + input: { + items: [ + { + sku: "FRAME", + title: "Frame", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "WHEEL", + title: "Wheel", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + { + sku: "SEAT", + title: "Seat", + location_levels: [ + { + stocked_quantity: 100, + location_id: stockLocations[0].id, + }, + ], + }, + ], + }, + }) + + // TODO create the product + } +) +``` + +You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). + +Then, you create the inventory items that the product variant consists of. + +Next, create the product and pass the inventory item's IDs to the product's variant: + +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... + + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bicycle", + variants: [ + { + title: "Bicycle - Small", + prices: [ + { + amount: 100, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + shipping_profile_id: "sp_123", + }, + ], + }, + }) + } +) +``` + +You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. + +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Bundled Products + +While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products. + +Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. + +![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) + +You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. + +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. + +![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) + +### Create Bundled Product + +You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. + +Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: + +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shirt", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Pants", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Pants", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + { + title: "Shoes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) + + // TODO re-retrieve with inventory + } +) +``` + +You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). + +Next, retrieve the products again but with variant information: + +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const productIds = transform({ + products, + }, (data) => data.products.map((product) => product.id)) + + // @ts-ignore + const { data: productsWithInventory } = useQueryGraphStep({ + entity: "product", + fields: [ + "variants.*", + "variants.inventory_items.*", + ], + filters: { + id: productIds, + }, + }) + + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) + + // create bundled product + } +) +``` + +Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. + +Finally, create the bundled product: + +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + shipping_profile_id: "sp_123", + variants: [ + { + title: "Bundle", + prices: [ + { + amount: 30, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + inventory_items: inventoryItemIds, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }).config({ name: "create-bundled-product" }) + } +) +``` + +The bundled product has the same inventory items as those of the products part of the bundle. + +You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + + +# Links between Currency Module and Other Modules + +This document showcases the module links defined between the Currency Module and other Commerce Modules. + +## Summary + +The Currency Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|StoreCurrency|Currency|Read-only - has one|Learn more| + +*** + +## Store Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. + +### Retrieve with Query + +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: + +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + + +# Links between Inventory Module and Other Modules + +This document showcases the module links defined between the Inventory Module and other Commerce Modules. + +## Summary + +The Inventory Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| +|InventoryLevel|StockLocation|Read-only - has many|Learn more| + +*** + +## Product Module + +Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. + +![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) + +A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. + +Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +### Retrieve with Query + +To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: [ + "variants.*", + ], +}) + +// inventoryItems[0].variants +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryItems } = useQueryGraphStep({ + entity: "inventory_item", + fields: [ + "variants.*", + ], +}) + +// inventoryItems[0].variants +``` + +### Manage with Link + +To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) +``` + +*** + +## Stock Location Module + +Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. + +### Retrieve with Query + +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + + # Cart Concepts In this document, you’ll get an overview of the main concepts of a cart. @@ -21571,81 +22864,6 @@ If the fulfillment provider requires additional custom data to be passed along f The `data` property is an object used to store custom data relevant later for fulfillment. -# Tax Lines in Cart Module - -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. - -![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. - -*** - -## Retrieve Tax Lines - -When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. - -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) - -// retrieve the tax lines -const taxLines = await taxModuleService.getTaxLines( - [ - ...(cart.items as TaxableItemDTO[]), - ...(cart.shipping_methods as TaxableShippingDTO[]), - ], - { - address: { - ...cart.shipping_address, - country_code: - cart.shipping_address.country_code || "us", - }, - } -) -``` - -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: - -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) - -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` - - # Links between Cart Module and Other Modules This document showcases the module links defined between the Cart Module and other Commerce Modules. @@ -21658,14 +22876,14 @@ Read-only links are used to query data across modules, but the relations aren't |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|| in |Read-only - has one|| -| in ||Stored - one-to-one|| -|| in |Stored - one-to-one|| -|| in |Read-only - has one|| -|| in |Read-only - has one|| -|| in |Stored - many-to-many|| -|| in |Read-only - has one|| -|| in |Read-only - has one|| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Cart|Stored - one-to-one|Learn more| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|LineItem|Product|Read-only - has one|Learn more| +|LineItem|ProductVariant|Read-only - has one|Learn more| +|Cart|Promotion|Stored - many-to-many|Learn more| +|Cart|Region|Read-only - has one|Learn more| +|Cart|SalesChannel|Read-only - has one|Learn more| *** @@ -22203,60 +23421,78 @@ await cartModuleService.setShippingMethodAdjustments( ``` -# Links between Currency Module and Other Modules +# Tax Lines in Cart Module -This document showcases the module links defined between the Currency Module and other Commerce Modules. +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -## Summary +## What are Tax Lines? -The Currency Module has the following links to other modules: +A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Read-only - has one|| +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) *** -## Store Module +## Tax Inclusivity -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. -Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. -### Retrieve with Query +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. -### query.graph +![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. + +*** + +## Retrieve Tax Lines + +When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. ```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", ], }) -// stores[0].supported_currencies[0].currency +// retrieve the tax lines +const taxLines = await taxModuleService.getTaxLines( + [ + ...(cart.items as TaxableItemDTO[]), + ...(cart.shipping_methods as TaxableShippingDTO[]), + ], + { + address: { + ...cart.shipping_address, + country_code: + cart.shipping_address.country_code || "us", + }, + } +) ``` -### useQueryGraphStep +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: ```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies[0].currency +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) ``` @@ -23496,503 +24732,25 @@ The page shows the user password fields to enter their new password, then submit - [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) -# Fulfillment Concepts +# Payment Module Options -In this document, you’ll learn about some basic fulfillment concepts. +In this document, you'll learn about the options of the Payment Module. -## Fulfillment Set +## All Module Options -A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. - -A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. - -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| +|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| +|\`providers\`|An array of payment providers to install and register. Learn more |No|-| *** -## Service Zone +## providers Option -A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. +The `providers` option is an array of payment module providers. -A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. - -![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) - -A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -*** - -## Shipping Profile - -A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. - -A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. - - -# Fulfillment Module Provider - -In this document, you’ll learn what a fulfillment module provider is. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. - -## What’s a Fulfillment Module Provider? - -A fulfillment module provider handles fulfilling items, typically using a third-party integration. - -Fulfillment module providers registered in the Fulfillment Module's [options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) are stored and represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md). - -*** - -## Configure Fulfillment Providers - -The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. - -Learn more about the `providers` option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md). - -*** - -## How to Create a Fulfillment Provider? - -Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. - - -# Item Fulfillment - -In this document, you’ll learn about the concepts of item fulfillment. - -## Fulfillment Data Model - -A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). - -*** - -## Fulfillment Processing by a Fulfillment Provider - -A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. - -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. - -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) - -*** - -## data Property - -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. - -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. - -*** - -## Fulfillment Items - -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. - -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. - -![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) - -*** - -## Fulfillment Label - -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. - -*** - -## Fulfillment Status - -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: - -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. - - -# Links between Fulfillment Module and Other Modules - -This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. - -## Summary - -The Fulfillment Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - one-to-many|| -| in ||Stored - one-to-many|| -| in ||Stored - many-to-one|| -| in ||Stored - many-to-one|| -| in ||Stored - one-to-many|| -| in ||Stored - one-to-many|| - -*** - -## Order Module - -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. - -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) - -### Retrieve with Query - -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -To retrieve the return, pass `return.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -*** - -## Pricing Module - -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: - -### query.graph - -```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. - -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). - -### Retrieve with Query - -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: - -### query.graph - -```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### Manage with Link - -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -*** - -## Stock Location Module - -The Stock Location Module provides features to manage stock locations in a store. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: - -To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - - -# Fulfillment Module Options - -In this document, you'll learn about the options of the Fulfillment Module. - -## providers - -The `providers` option is an array of fulfillment module providers. - -When the Medusa application starts, these providers are registered and can be used to process fulfillments. +When the Medusa application starts, these providers are registered and can be used to process payments. For example: @@ -24005,14 +24763,14 @@ module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/fulfillment", + resolve: "@medusajs/medusa/payment", options: { providers: [ { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", options: { - // provider options... + // ... }, }, ], @@ -24024,1533 +24782,11 @@ module.exports = defineConfig({ The `providers` option is an array of objects that accept the following properties: -- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. - `id`: A string indicating the provider's unique name or ID. - `options`: An optional object of the module provider's options. -# Shipping Option - -In this document, you’ll learn about shipping options and their rules. - -## What’s a Shipping Option? - -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. - -When the customer places their order, they choose a shipping option to be used to fulfill their items. - -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). - -*** - -## Service Zone Restrictions - -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. - -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. - -![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) - -Service zones can be more restrictive, such as restricting to certain cities or province codes. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) - -*** - -## Shipping Option Rules - -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. - -You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. - -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: - -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. - - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). -- `value`: One or more values. - -![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) - -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. - -![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) - -*** - -## Shipping Profile and Types - -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). - -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. - -*** - -## data Property - -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. - -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. - - -# Inventory Module in Medusa Flows - -This document explains how the Inventory Module is used within the Medusa application's flows. - -## Product Variant Creation - -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. - -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) - -*** - -## Add to Cart - -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. - -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) - -*** - -## Order Placed - -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. - -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) - -*** - -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) - -*** - -## Order Return - -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. - -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) - -### Dismissed Returned Items - -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. - - -# Inventory Concepts - -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. - -## InventoryItem - -An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. - -The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. - -![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) - -### Inventory Shipping Requirement - -An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. - -When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). - -*** - -## InventoryLevel - -An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. - -It has three quantity-related properties: - -- `stocked_quantity`: The available stock quantity of an item in the associated location. -- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. - -### Associated Location - -The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. - -*** - -## ReservationItem - -A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. - -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. - - -# Inventory Kits - -In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. - -Refer to the following user guides to learn how to use the Medusa Admin dashboard to: - -- [Create Multi-Part Products](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md). -- [Create Bundled Products](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md). - -## What is an Inventory Kit? - -An inventory kit is a collection of inventory items that are linked to a single product variant. These inventory items can be used to represent different parts of a product, or to represent a bundle of products. - -The Medusa application links inventory items from the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to product variants in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). Each variant can have multiple inventory items, and these inventory items can be re-used or shared across variants. - -Using inventory kits, you can implement use cases like: - -- [Multi-part products](#multi-part-products): A product that consists of multiple parts, each with its own inventory item. -- [Bundled products](#bundled-products): A product that is sold as a bundle, where each variant in the bundle product can re-use the inventory items of another product that should be sold as part of the bundle. - -*** - -## Multi-Part Products - -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. - -To implement this in Medusa, you can: - -- Create inventory items for each of the different parts. -- For each bicycle product, add a variant whose inventory kit consists of the inventory items of each of the parts. - -Then, whenever a customer purchases a bicycle, the inventory of each part is updated accordingly. You can also use the `required_quantity` of the variant's inventory items to set how much quantity is consumed of the part's inventory when a bicycle is sold. For example, the bicycle's wheels require 2 wheels inventory items to be sold when a bicycle is sold. - -![Diagram showcasing how a variant is linked to multi-part inventory items](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414257/Medusa%20Resources/multi-part-product_kepbnx.jpg) - -### Create Multi-Part Product - -Using the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/multi-part/index.html.md), you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: - -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // Alternatively, you can create a stock location - const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: ["*"], - filters: { - name: "European Warehouse", - }, - }) - - const inventoryItems = createInventoryItemsWorkflow.runAsStep({ - input: { - items: [ - { - sku: "FRAME", - title: "Frame", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "WHEEL", - title: "Wheel", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - { - sku: "SEAT", - title: "Seat", - location_levels: [ - { - stocked_quantity: 100, - location_id: stockLocations[0].id, - }, - ], - }, - ], - }, - }) - - // TODO create the product - } -) -``` - -You start by retrieving the stock location to create the inventory items in. Alternatively, you can [create a stock location](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md). - -Then, you create the inventory items that the product variant consists of. - -Next, create the product and pass the inventory item's IDs to the product's variant: - -```ts highlights={multiPartHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - // ... - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... - - const inventoryItemIds = transform({ - inventoryItems, - }, (data) => { - return data.inventoryItems.map((inventoryItem) => { - return { - inventory_item_id: inventoryItem.id, - // can also specify required_quantity - } - }) - }) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bicycle", - variants: [ - { - title: "Bicycle - Small", - prices: [ - { - amount: 100, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - shipping_profile_id: "sp_123", - }, - ], - }, - }) - } -) -``` - -You prepare the inventory item IDs to pass to the variant using [transform](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK, then pass these IDs to the created product's variant. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Bundled Products - -While inventory kits support bundled products, some features like custom pricing for a bundle or separate fulfillment for a bundle's items are not supported. To support those features, follow the [Bundled Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/bundled-products/examples/standard/index.html.md) tutorial to learn how to customize the Medusa application to add bundled products. - -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. - -![Diagram showcasing products each having their own variants and inventory](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414787/Medusa%20Resources/bundled-product-1_vmzewk.jpg) - -You can do that by creating a product, where each variant re-uses the inventory items of each of the shirt, pants, and shoes products. - -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. - -![Diagram showcasing a bundled product using the same inventory as the products part of the bundle](https://res.cloudinary.com/dza7lstvk/image/upload/v1736414780/Medusa%20Resources/bundled-product_x94ca1.jpg) - -### Create Bundled Product - -You can create a bundled product in the [Medusa Admin](https://docs.medusajs.com/user-guide/products/create/bundle/index.html.md) by creating the products part of the bundle first, each having its own inventory items. Then, you create the bundled product whose variant(s) have inventory kits composed of inventory items from each of the products part of the bundle. - -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the products part of the bundle: - -```ts highlights={bundledHighlights1} -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shirt", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Pants", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Pants", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - { - title: "Shoes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) - - // TODO re-retrieve with inventory - } -) -``` - -You create three products and enable `manage_inventory` for their variants, which will create a default inventory item. You can also create the inventory item first for more control over the quantity as explained in [the previous section](#create-multi-part-product). - -Next, retrieve the products again but with variant information: - -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" - -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const productIds = transform({ - products, - }, (data) => data.products.map((product) => product.id)) - - // @ts-ignore - const { data: productsWithInventory } = useQueryGraphStep({ - entity: "product", - fields: [ - "variants.*", - "variants.inventory_items.*", - ], - filters: { - id: productIds, - }, - }) - - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) - - // create bundled product - } -) -``` - -Using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), you retrieve the product again with the inventory items of each variant. Then, you prepare the inventory items to pass to the bundled product's variant. - -Finally, create the bundled product: - -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bundled Clothes", - shipping_profile_id: "sp_123", - variants: [ - { - title: "Bundle", - prices: [ - { - amount: 30, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - inventory_items: inventoryItemIds, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }).config({ name: "create-bundled-product" }) - } -) -``` - -The bundled product has the same inventory items as those of the products part of the bundle. - -You can now [execute the workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows#3-execute-the-workflow/index.html.md) in [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [scheduled jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md), or [subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - - -# Links between Inventory Module and Other Modules - -This document showcases the module links defined between the Inventory Module and other Commerce Modules. - -## Summary - -The Inventory Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - many-to-many|| -|| in |Read-only - has many|| - -*** - -## Product Module - -Each product variant has different inventory details. Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. - -![A diagram showcasing an example of how data models from the Inventory and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658720/Medusa%20Resources/inventory-product_ejnray.jpg) - -A product variant whose `manage_inventory` property is enabled has an associated inventory item. Through that inventory's items relations in the Inventory Module, you can manage and check the variant's inventory quantity. - -Learn more about product variant's inventory management in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -### Retrieve with Query - -To retrieve the product variants of an inventory item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variants.*` in `fields`: - -### query.graph - -```ts -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems[0].variants -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryItems } = useQueryGraphStep({ - entity: "inventory_item", - fields: [ - "variants.*", - ], -}) - -// inventoryItems[0].variants -``` - -### Manage with Link - -To manage the variants of an inventory item, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` - -*** - -## Stock Location Module - -Medusa defines a read-only link between the `InventoryLevel` data model and the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md)'s `StockLocation` data model. This means you can retrieve the details of an inventory level's stock locations, but you don't manage the links in a pivot table in the database. The stock location of an inventory level is determined by the `location_id` property of the `InventoryLevel` data model. - -### Retrieve with Query - -To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph - -```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels[0].stock_locations -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels[0].stock_locations -``` - - -# Pricing Concepts - -In this document, you’ll learn about the main concepts in the Pricing Module. - -## Price Set - -A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). - -Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) - -*** - -## Price List - -A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. - -A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. - -Its associated prices are represented by the `Price` data model. - - -# Links between Pricing Module and Other Modules - -This document showcases the module links defined between the Pricing Module and other Commerce Modules. - -## Summary - -The Pricing Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - one-to-one|| -| in ||Stored - one-to-one|| - -*** - -## Fulfillment Module - -The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -The Product Module doesn't store or manage the prices of product variants. - -Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. - -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) - -So, when you want to add prices for a product variant, you create a price set and add the prices to it. - -You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. - -### Retrieve with Query - -To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### Manage with Link - -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - - -# Prices Calculation - -In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. - -## calculatePrices Method - -The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. - -It returns a price object with the best matching price for each price set. - -### Calculation Context - -The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. - -For example: - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) -``` - -In this example, you retrieve the prices in a price set for the specified currency code and region ID. - -### Returned Price Object - -For each price set, the `calculatePrices` method selects two prices: - -- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. -- An original price, which is either: - - The same price as the calculated price if the price list it belongs to is of type `override`; - - Or a price that doesn't belong to a price list and best matches the specified context. - -Both prices are returned in an object that has the following properties: - -- id: (\`string\`) The ID of the price set from which the price was selected. -- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. -- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. -- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. -- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. -- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. -- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) -- calculated\_price: (\`object\`) The calculated price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. -- original\_price: (\`object\`) The original price's price details. - - - id: (\`string\`) The ID of the price. - - - price\_list\_id: (\`string\`) The ID of the associated price list. - - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. - - - min\_quantity: (\`number\`) The price's min quantity condition. - - - max\_quantity: (\`number\`) The price's max quantity condition. - -*** - -## Examples - -Consider the following price set: - -```ts -const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - // default price - { - amount: 500, - currency_code: "EUR", - rules: {}, - }, - // prices with rules - { - amount: 400, - currency_code: "EUR", - rules: { - region_id: "reg_123", - }, - }, - { - amount: 450, - currency_code: "EUR", - rules: { - city: "krakow", - }, - }, - { - amount: 500, - currency_code: "EUR", - rules: { - city: "warsaw", - region_id: "reg_123", - }, - }, - { - amount: 200, - currency_code: "EUR", - min_quantity: 100, - } - ], -}) -``` - -### Default Price Selection - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` - -### Result - -### Calculate Prices with Rules - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) -``` - -### Result - -### Tiered Pricing Selection - -### Code - -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - cart: { - items: [ - { - id: "item_1", - quantity: 200, - // assuming the price set belongs to this variant - variant_id: "variant_1", - // ... - } - ], - // ... - } - } - } -) -``` - -### Result - -### Price Selection with Price List - -### Code - -```ts -const priceList = pricingModuleService.createPriceLists([{ - title: "Summer Price List", - description: "Price list for summer sale", - starts_at: Date.parse("01/10/2023").toString(), - ends_at: Date.parse("31/10/2023").toString(), - rules: { - region_id: ['PL'] - }, - type: "sale", - prices: [ - { - amount: 400, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - { - amount: 450, - currency_code: "EUR", - price_set_id: priceSet.id, - }, - ], -}]); - -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) -``` - -### Result - - -# Price Tiers and Rules - -In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. - -## Tiered Pricing - -Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: - -- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. -- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. - -This is useful to set tiered pricing for resources like product variants and shipping options. - -For example, you can set a variant's price to: - -- `$10` by default. -- `$8` when the customer adds `10` or more of the variant to the cart. -- `$6` when the customer adds `20` or more of the variant to the cart. - -These price definitions would look like this: - -```json title="Example Prices" -[ - // default price - { - "amount": 10, - "currency_code": "usd", - }, - { - "amount": 8, - "currency_code": "usd", - "min_quantity": 10, - "max_quantity": 19, - }, - { - "amount": 6, - "currency_code": "usd", - "min_quantity": 20, - }, -], -``` - -### How to Create Tiered Prices? - -When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. - -For example: - -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. - -### Using Medusa Workflows - -```ts highlights={tieredPricingHighlights} -const { result } = await createProductsWorkflow(container) - .run({ - input: { - products: [{ - variants: [{ - id: "variant_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], - // ... - }] - }], - // ... - } - }) -``` - -### Using the Pricing Module - -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - amount: 10, - currency_code: "usd", - }, - // tiered prices - { - amount: 8, - currency_code: "usd", - min_quantity: 10, - max_quantity: 19, - }, - { - amount: 6, - currency_code: "usd", - min_quantity: 20, - }, - ], -}) -``` - -In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. - -### How are Tiered Prices Applied? - -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply. - -For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. - -The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. - -If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. - -*** - -## Price Rule - -You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. - -Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). - -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. - -For exmaple, you create a price restricted to `10557` zip codes. - -![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) - -A price can have multiple price rules. - -For example, a price can be restricted by a region and a zip code. - -![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) - -### Price List Rules - -Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). - -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. - -![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) - -### How to Create Prices with Rules? - -When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. - -For example: - -For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. - -### Using Medusa Workflows - -```ts highlights={workflowHighlights} -const { result } = await createShippingOptionsWorkflow(container) - .run({ - input: [{ - name: "Standard Shipping", - service_zone_id: "serzo_123", - shipping_profile_id: "sp_123", - provider_id: "prov_123", - type: { - label: "Standard", - description: "Standard shipping", - code: "standard", - }, - price_type: "flat", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, - }, - }, - ], - }], - }) -``` - -### Using the Pricing Module - -```ts -const priceSet = await pricingModule.addPrices({ - priceSetId: "pset_1", - prices: [ - // default price - { - currency_code: "usd", - amount: 10, - rules: {}, - }, - // price if cart total >= $100 - { - currency_code: "usd", - amount: 0, - rules: { - item_total: { - operator: "gte", - value: 100, - }, - }, - }, - ], -}) -``` - -In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. - -### How is the Price Rule Applied? - -The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. - -For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: - -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "item_total": { - "operator": "gte", - "value": 100, - } - } -} -``` - -The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. - -You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: - -```json -{ - "currency_code": "usd", - "amount": 0, - "rules": { - "customer.group.id": { - "operator": "eq", - "value": "cusgrp_123" - } - } -} -``` - -In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. - -These same rules apply to product variant prices as well, or any other resource that has a price. - - -# Tax-Inclusive Pricing - -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. - -## What is Tax-Inclusive Pricing? - -A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. - -For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. - -*** - -## How is Tax-Inclusive Pricing Set? - -The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: - -- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. -- `value`: The attribute’s value. For example, `reg_123` or `usd`. - -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. - -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. - -For example: - -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` - -In this example, tax-inclusivity is enabled for the `USD` currency code. - -*** - -## Tax-Inclusive Pricing in Price Calculation - -### Tax Context - -As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. - -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. - -### Returned Tax Properties - -The `calculatePrices` method returns two properties related to tax-inclusivity: - -Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - -- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. -- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. - -A price is considered tax-inclusive if: - -1. It belongs to the region or currency code specified in the calculation context; -2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. - -### Tax Context Precedence - -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: - -- both the `region_id` and `currency_code` are provided in the calculation context; -- the selected price belongs to the region; -- and the region has a price preference - - -# Account Holders and Saved Payment Methods - -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. - -Account holders are available starting from Medusa `v2.5.0`. - -## What's an Account Holder? - -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. - -It holds fields retrieved from the third-party provider, such as: - -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. - -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. - -### Relation between Account Holder and Customer - -The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. - -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. - -Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. - -*** - -## Save Payment Methods - -If a payment provider supports saving payment methods for a customer, they must implement the following methods: - -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). - -*** - -## Account Holder in Medusa Payment Flows - -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. - -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. - -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). - - # Links between Payment Module and Other Modules This document showcases the module links defined between the Payment Module and other Commerce Modules. @@ -25561,12 +24797,12 @@ The Payment Module has the following links to other modules: |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Stored - one-to-one|| -| in ||Stored - many-to-many|| -| in ||Stored - one-to-many|| -| in ||Stored - one-to-many|| -| in ||Stored - one-to-many|| -| in ||Stored - many-to-many|| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Order|PaymentCollection|Stored - one-to-many|Learn more| +|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| +|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| +|Region|PaymentProvider|Stored - many-to-many|Learn more| *** @@ -25897,61 +25133,6 @@ createRemoteLinkStep({ ``` -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. - -## All Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| - -*** - -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - # Payment In this document, you’ll learn what a payment is and how it's created, captured, and refunded. @@ -26191,37 +25372,53 @@ You can then: Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. -# Payment Session +# Account Holders and Saved Payment Methods -In this document, you’ll learn what a payment session is. +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. -## What's a Payment Session? +Account holders are available starting from Medusa `v2.5.0`. -A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. +## What's an Account Holder? -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. +An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) +It holds fields retrieved from the third-party provider, such as: + +- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. +- `data`: Data returned by the payment provider when the account holder is created. + +A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. + +### Relation between Account Holder and Customer + +The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. + +This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. + +Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. *** -## data Property +## Save Payment Methods -Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. +If a payment provider supports saving payment methods for a customer, they must implement the following methods: -For example, the customer's ID in Stripe is stored in the `data` property. +- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. +- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. +- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. +- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). *** -## Payment Session Status +## Account Holder in Medusa Payment Flows -The `status` property of a payment session indicates its current status. Its value can be: +In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. -- `pending`: The payment session is awaiting authorization. -- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. -- `authorized`: The payment session is authorized. -- `error`: An error occurred while authorizing the payment. -- `canceled`: The authorization of the payment session has been canceled. +Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). # Payment Module Provider @@ -26275,6 +25472,39 @@ When the Medusa application starts and registers the payment providers, it also This data model is used to reference a payment provider and determine whether it’s installed in the application. +# Payment Session + +In this document, you’ll learn what a payment session is. + +## What's a Payment Session? + +A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. + +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. + +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) + +*** + +## data Property + +Payment providers may need additional data to process the payment later. The `PaymentSession` data model has a `data` property used to store that data. + +For example, the customer's ID in Stripe is stored in the `data` property. + +*** + +## Payment Session Status + +The `status` property of a payment session indicates its current status. Its value can be: + +- `pending`: The payment session is awaiting authorization. +- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. +- `authorized`: The payment session is authorized. +- `error`: An error occurred while authorizing the payment. +- `canceled`: The authorization of the payment session has been canceled. + + # Webhook Events In this document, you’ll learn how the Payment Module supports listening to webhook events. @@ -26311,308 +25541,385 @@ If the event's details indicate that the payment should be captured, then the [c After the payment webhook actions are processed and the payment is authorized or captured, the Medusa application completes the cart associated with the payment's collection if it's not completed yet. -# Order Concepts +# Promotion Actions -In this document, you’ll learn about orders and related concepts +In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). -## Order Items +## computeActions Method -The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. +The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) - -### Item’s Product Details - -The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. +Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. *** -## Order’s Shipping Method +## Action Types -An order has one or more shipping methods used to handle item shipment. +### `addItemAdjustment` Action -Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). +The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. -![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) +This action has the following format: -### data Property +```ts +export interface AddItemAdjustmentAction { + action: "addItemAdjustment" + item_id: string + amount: number + code: string + description?: string +} +``` -When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. +This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. +### `removeItemAdjustment` Action + +The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. + +The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveItemAdjustmentAction { + action: "removeItemAdjustment" + adjustment_id: string + description?: string + code: string +} +``` + +This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. + +### `addShippingMethodAdjustment` Action + +The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. + +This action has the following format: + +```ts +export interface AddShippingMethodAdjustment { + action: "addShippingMethodAdjustment" + shipping_method_id: string + amount: number + code: string + description?: string +} +``` + +This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `removeShippingMethodAdjustment` Action + +The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. + +The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. + +This action has the following format: + +```ts +export interface RemoveShippingMethodAdjustment { + action: "removeShippingMethodAdjustment" + adjustment_id: string + code: string +} +``` + +When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. + +### `campaignBudgetExceeded` Action + +When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. + +This action has the following format: + +```ts +export interface CampaignBudgetExceededAction { + action: "campaignBudgetExceeded" + code: string +} +``` + +Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. + + +# Application Method + +In this document, you'll learn what an application method is. + +## What is an Application Method? + +The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: + +|Property|Purpose| +|---|---| +|\`type\`|Does the promotion discount a fixed amount or a percentage?| +|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| +|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| + +## Target Promotion Rules + +When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. + +The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. + +![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) + +In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. *** -## Order Totals +## Buy Promotion Rules -The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). +When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. + +The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. + +![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) + +In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. + + +# Promotion Concepts + +In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. + +## What is a Promotion? + +A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. + +A promotion has two types: + +- `standard`: A standard promotion with rules. +- `buyget`: “A buy X get Y” promotion with rules. + +|\`standard\`|\`buyget\`| +|---|---| +|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| +|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| +|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| + +The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. *** -## Order Payments +## Promotion Rules -Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). -An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. +For example, you can create a promotion that only customers of the `VIP` customer group can use. -Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). +![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) +A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. -# Order Exchange +The expected value for the attribute is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. -In this document, you’ll learn about order exchanges. +When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. +For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. -## What is an Exchange? +### Flexible Rules -An exchange is the replacement of an item that the customer ordered with another. +The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. +For example, to restrict the promotion to only `VIP` and `B2B` customer groups: -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. +- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. +- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. + +![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) + +In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. *** -## Returned and New Items +## How to Apply Rules on a Promotion? -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. +### Using Workflows -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). +If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md). -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. +For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createPromotionsWorkflow/index.html.md): + +```ts +const { result } = await createPromotionsWorkflow(container) + .run({ + input: { + promotionsData: [{ + code: "10OFF", + type: "standard", + status: "active", + application_method: { + type: "percentage", + target_type: "items", + allocation: "across", + value: 10, + currency_code: "usd", + }, + rules: [ + { + attribute: "customer.group.id", + operator: "eq", + values: [ + "cusgrp_123", + ], + }, + ], + }], + }, + }) +``` + +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. + +### Using Promotion Module's Service + +For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. + +If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods. + +For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/resources/references/promotion/createPromotions/index.html.md) method: + +```ts +const promotions = await promotionModuleService.createPromotions([ + { + code: "50OFF", + type: "standard", + status: "active", + application_method: { + type: "percentage", + target_type: "items", + value: 50, + }, + rules: [ + { + attribute: "customer.group.id", + operator: "eq", + values: [ + "cusgrp_123", + ], + }, + ], + }, +]) +``` + +In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. + +### How is the Promotion Rule Applied? + +A promotion is applied on a resource if its attributes match the promotion's rules. + +For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer: + +```json +{ + "code": "10OFF", + "type": "standard", + "status": "active", + "application_method": { + "type": "percentage", + "target_type": "items", + "allocation": "across", + "value": 10, + "currency_code": "usd" + }, + "rules": [ + { + "attribute": "customer_id", + "operator": "eq", + "values": [ + "cus_123" + ] + } + ] +} +``` + +When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`. + + +# Campaign + +In this document, you'll learn about campaigns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. + +## What is a Campaign? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + +![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) *** -## Exchange Shipping Methods +## Campaign Limits -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. -The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). +There are two types of budgets: -*** +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. -## Exchange Payment +![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. -|Condition|Result| -|---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| +# Links between Promotion Module and Other Modules -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -*** - -## How Exchanges Impact an Order’s Version - -When an exchange is confirmed, the order’s version is incremented. - - -# Order Claim - -In this document, you’ll learn about order claims. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. - -## What is a Claim? - -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. - -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. - -*** - -## Claim Type - -The `Claim` data model has a `type` property whose value indicates the type of the claim: - -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. - -*** - -## Old and Replacement Items - -When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. - -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). - -*** - -## Claim Shipping Methods - -A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). - -*** - -## Claim Refund - -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. - -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. - -*** - -## How Claims Impact an Order’s Version - -When a claim is confirmed, the order’s version is incremented. - - -# Order Edit - -In this document, you'll learn about order edits. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. - -## What is an Order Edit? - -A merchant can edit an order to add new items or change the quantity of existing items in the order. - -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). - -The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. - -In the case of an order edit, the `OrderChange`'s type is `edit`. - -*** - -## Add Items in an Order Edit - -When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). - -Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. - -So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. - -*** - -## Update Items in an Order Edit - -A merchant can update an existing item's quantity or price. - -This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. - -*** - -## Shipping Methods of New Items in the Edit - -Adding new items to the order requires adding shipping methods for those items. - -These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` - -*** - -## How Order Edits Impact an Order’s Version - -When an order edit is confirmed, the order’s version is incremented. - -*** - -## Payments and Refunds for Order Edit Changes - -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. - -This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). - - -# Links between Order Module and Other Modules - -This document showcases the module links defined between the Order Module and other Commerce Modules. +This document showcases the module links defined between the Promotion Module and other Commerce Modules. ## Summary -The Order Module has the following links to other modules: +The Promotion Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|| in |Read-only - has one|| -|| in |Stored - one-to-one|| -|| in |Stored - one-to-many|| -|| in |Stored - one-to-many|| -|| in |Stored - one-to-many|| -|| in |Stored - one-to-many|| -|| in |Stored - one-to-many|| -|| in |Read-only - has many|| -|| in |Stored - many-to-many|| -|| in |Read-only - has one|| -|| in |Read-only - has one|| - -*** - -## Customer Module - -Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. - -### Retrieve with Query - -To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders[0].customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders[0].customer -``` +|Cart|Promotion|Stored - many-to-many|Learn more| +|LineItemAdjustment|Promotion|Read-only - has one|Learn more| +|Order|Promotion|Stored - many-to-many|Learn more| *** ## Cart Module -The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. -Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) +Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. ### Retrieve with Query -To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: +To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "cart.*", + "carts.*", ], }) -// orders[0].cart +// promotions[0].carts ``` ### useQueryGraphStep @@ -26622,19 +25929,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "cart.*", + "carts.*", ], }) -// orders[0].cart +// promotions[0].carts ``` ### Manage with Link -To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -26644,12 +25951,12 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.CART]: { cart_id: "cart_123", }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, }) ``` @@ -26662,233 +25969,18 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, [Modules.CART]: { cart_id: "cart_123", }, -}) -``` - -*** - -## Fulfillment Module - -A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) - -### Retrieve with Query - -To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: - -To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "fulfillments.*", - ], -}) - -// orders[0].fulfillments -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "fulfillments.*", - ], -}) - -// orders[0].fulfillments -``` - -### Manage with Link - -To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", + [Modules.PROMOTION]: { + promotion_id: "promo_123", }, }) ``` *** -## Payment Module - -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. - -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. - -![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) - -### Retrieve with Query - -To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "payment_collections.*", - ], -}) - -// orders[0].payment_collections -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "payment_collections.*", - ], -}) - -// orders[0].payment_collections -``` - -### Manage with Link - -To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines read-only links between: - -- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. -- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "order_line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -*** - -## Promotion Module +## Order Module An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. @@ -26896,19 +25988,19 @@ An order is associated with the promotion applied on it. Medusa defines a link b ### Retrieve with Query -To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: +To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: promotions } = await query.graph({ + entity: "promotion", fields: [ - "promotion.*", + "orders.*", ], }) -// orders[0].promotion +// promotions[0].orders ``` ### useQueryGraphStep @@ -26918,14 +26010,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", fields: [ - "promotion.*", + "orders.*", ], }) -// orders[0].promotion +// promotions[0].orders ``` ### Manage with Link @@ -26967,415 +26059,6 @@ createRemoteLinkStep({ }) ``` -*** - -## Region Module - -Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. - -### Retrieve with Query - -To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -*** - -## Sales Channel Module - -Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. - -### Retrieve with Query - -To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders[0].sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) - -// orders[0].sales_channel -``` - - -# Promotions Adjustments in Orders - -In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. - -## What are Adjustment Lines? - -An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. - -The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. - -![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg) - -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. - -The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. - -*** - -## Discountable Option - -The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. - -When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. - -*** - -## Promotion Actions - -When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. - -Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). - -```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" - -// ... - -// retrieve the order -const order = await orderModuleService.retrieveOrder("ord_123", { - relations: [ - "items.item.adjustments", - "shipping_methods.shipping_method.adjustments", - ], -}) -// retrieve the line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -order.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - ...item.detail, - adjustments: filteredAdjustments, - }) - } -}) - -//retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -order.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) - -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - // TODO infer from cart or region - currency_code: "usd", - } -) -``` - -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. - -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -await orderModuleService.setOrderLineItemAdjustments( - order.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) - -await orderModuleService.setOrderShippingMethodAdjustments( - order.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` - - -# Order Change - -In this document, you'll learn about the Order Change data model and possible actions in it. - -## OrderChange Data Model - -The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. - -Its `change_type` property indicates what the order change is created for: - -1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). -2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). -3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). -4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -Once the order change is confirmed, its changes are applied on the order. - -*** - -## Order Change Actions - -The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). - -The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. - -The following table lists the possible `action` values that Medusa uses and what `details` they carry. - -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| - - -# Order Versioning - -In this document, you’ll learn how an order and its details are versioned. - -## What's Versioning? - -Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. - -When changes are made on an order, such as an item is added or returned, the order's version changes. - -*** - -## version Property - -The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. - -Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. - -*** - -## How the Version Changes - -When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: - -1. The version of the order and its summary is incremented. -2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. - -When the order is retrieved, only the related data having the same version is retrieved. - - -# Order Return - -In this document, you’ll learn about order returns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. - -## What is a Return? - -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). - -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. - -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) - -Once the merchant receives the returned items, they mark the return as received. - -*** - -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). - -The `ReturnItem` model has two properties storing the item's quantity: - -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. - -*** - -## Return Shipping Methods - -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). - -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. - -*** - -## Refund Payment - -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. - -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. - -*** - -## Returns in Exchanges and Claims - -When a merchant creates an exchange or a claim, it includes returning items from the customer. - -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. - -*** - -## How Returns Impact an Order’s Version - -The order’s version is incremented when: - -1. A return is requested. -2. A return is marked as received. - - -# Tax Lines in Order Module - -In this document, you’ll learn about tax lines in an order. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. - -The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. - -![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. - - -# Transactions - -In this document, you’ll learn about an order’s transactions and its use. - -## What is a Transaction? - -A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. - -Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. - -*** - -## Checking Outstanding Amount - -The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. - -```json -{ - "totals": { - "total": 30, - "subtotal": 30, - // ... - } -} -``` - -To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: - -|Condition|Result| -|---|---|---| -|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| -|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| -|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| - -*** - -## Transaction Reference - -The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). - -The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: - -- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. -- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. - # Links between Product Module and Other Modules @@ -27389,12 +26072,12 @@ Read-only links are used to query data across modules, but the relations aren't |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Read-only - has one|| -|| in |Stored - many-to-one|| -|| in |Stored - many-to-many|| -| in ||Read-only - has one|| -|| in |Stored - one-to-one|| -|| in |Stored - many-to-many|| +|LineItem|Product|Read-only - has one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|ProductVariant|InventoryItem|Stored - many-to-many|Learn more| +|OrderLineItem|Product|Read-only - has one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| +|Product|SalesChannel|Stored - many-to-many|Learn more| *** @@ -27941,6 +26624,1842 @@ The following guides provide more details on inventory management in the Medusa - [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). +# Order Concepts + +In this document, you’ll learn about orders and related concepts + +## Order Items + +The items purchased in the order are represented by the [OrderItem data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). An order can have multiple items. + +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712304722/Medusa%20Resources/order-order-items_uvckxd.jpg) + +### Item’s Product Details + +The details of the purchased products are represented by the [LineItem data model](https://docs.medusajs.com/references/order/models/OrderLineItem/index.html.md). Not only does a line item hold the details of the product, but also details related to its price, adjustments due to promotions, and taxes. + +*** + +## Order’s Shipping Method + +An order has one or more shipping methods used to handle item shipment. + +Each shipping method is represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md) that holds its details. The shipping method is linked to the order through the [OrderShipping data model](https://docs.medusajs.com/references/order/models/OrderShipping/index.html.md). + +![A diagram showcasing the relation between an order and its items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719570409/Medusa%20Resources/order-shipping-method_tkggvd.jpg) + +### data Property + +When fulfilling the order, you can use a third-party fulfillment provider that requires additional custom data to be passed along from the order creation process. + +The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. + +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. + +*** + +## Order Totals + +The order’s total amounts (including tax total, total after an item is returned, etc…) are represented by the [OrderSummary data model](https://docs.medusajs.com/references/order/models/OrderSummary/index.html.md). + +*** + +## Order Payments + +Payments made on an order, whether they’re capture or refund payments, are recorded as transactions represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +An order can have multiple transactions. The sum of these transactions must be equal to the order summary’s total. Otherwise, there’s an outstanding amount. + +Learn more about transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). + + +# Order Edit + +In this document, you'll learn about order edits. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/edit/index.html.md) to learn how to edit an order's items using the dashboard. + +## What is an Order Edit? + +A merchant can edit an order to add new items or change the quantity of existing items in the order. + +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). + +The `OrderChange` data model is associated with any type of change, including a return or exchange. However, its `change_type` property distinguishes the type of change it's making. + +In the case of an order edit, the `OrderChange`'s type is `edit`. + +*** + +## Add Items in an Order Edit + +When the merchant adds new items to the order in the order edit, the item is added as an [OrderItem](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). + +Also, an `OrderChangeAction` is created. The [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md) represents a change made by an `OrderChange`, such as an item added. + +So, when an item is added, an `OrderChangeAction` is created with the type `ITEM_ADD`. In its `details` property, the item's ID, price, and quantity are stored. + +*** + +## Update Items in an Order Edit + +A merchant can update an existing item's quantity or price. + +This change is added as an `OrderChangeAction` with the type `ITEM_UPDATE`. In its `details` property, the item's ID, new price, and new quantity are stored. + +*** + +## Shipping Methods of New Items in the Edit + +Adding new items to the order requires adding shipping methods for those items. + +These shipping methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderItem/index.html.md). Also, an `OrderChangeAction` is created with the type `SHIPPING_ADD` + +*** + +## How Order Edits Impact an Order’s Version + +When an order edit is confirmed, the order’s version is incremented. + +*** + +## Payments and Refunds for Order Edit Changes + +Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. + +This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). + + +# Order Exchange + +In this document, you’ll learn about order exchanges. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. + +## What is an Exchange? + +An exchange is the replacement of an item that the customer ordered with another. + +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. + +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. + +*** + +## Returned and New Items + +When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. + +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. + +*** + +## Exchange Shipping Methods + +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). + +The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). + +*** + +## Exchange Payment + +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. + +|Condition|Result| +|---|---|---| +|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | +|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | +|\`difference\_due = 0\`|No payment processing is required.| + +Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +*** + +## How Exchanges Impact an Order’s Version + +When an exchange is confirmed, the order’s version is incremented. + + +# Links between Order Module and Other Modules + +This document showcases the module links defined between the Order Module and other Commerce Modules. + +## Summary + +The Order Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Order|Customer|Read-only - has one|Learn more| +|Order|Cart|Stored - one-to-one|Learn more| +|Order|Fulfillment|Stored - one-to-many|Learn more| +|Return|Fulfillment|Stored - one-to-many|Learn more| +|Order|PaymentCollection|Stored - one-to-many|Learn more| +|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| +|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| +|OrderLineItem|Product|Read-only - has many|Learn more| +|Order|Promotion|Stored - many-to-many|Learn more| +|Order|Region|Read-only - has one|Learn more| +|Order|SalesChannel|Read-only - has one|Learn more| + +*** + +## Customer Module + +Medusa defines a read-only link between the `Order` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of an order's customer, but you don't manage the links in a pivot table in the database. The customer of an order is determined by the `customer_id` property of the `Order` data model. + +### Retrieve with Query + +To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders[0].customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders[0].customer +``` + +*** + +## Cart Module + +The [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md) provides cart-management features. + +Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. + +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) + +### Retrieve with Query + +To retrieve the cart of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "cart.*", + ], +}) + +// orders[0].cart +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "cart.*", + ], +}) + +// orders[0].cart +``` + +### Manage with Link + +To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.CART]: { + cart_id: "cart_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.CART]: { + cart_id: "cart_123", + }, +}) +``` + +*** + +## Fulfillment Module + +A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) + +### Retrieve with Query + +To retrieve the fulfillments of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillments.*` in `fields`: + +To retrieve the fulfillments of a return, pass `fulfillments.*` in `fields`. + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "fulfillments.*", + ], +}) + +// orders[0].fulfillments +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "fulfillments.*", + ], +}) + +// orders[0].fulfillments +``` + +### Manage with Link + +To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +*** + +## Payment Module + +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. + +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. + +![A diagram showcasing an example of how data models from the Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) + +### Retrieve with Query + +To retrieve the payment collections of an order, order exchange, or order claim with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collections.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) + +// orders[0].payment_collections +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) + +// orders[0].payment_collections +``` + +### Manage with Link + +To manage the payment collections of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines read-only links between: + +- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `OrderLineItem` data model. +- the `OrderLineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `OrderLineItem` data model. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +*** + +## Promotion Module + +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. + +![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) + +### Retrieve with Query + +To retrieve the promotion applied on an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotion.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "promotion.*", + ], +}) + +// orders[0].promotion +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "promotion.*", + ], +}) + +// orders[0].promotion +``` + +### Manage with Link + +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Region Module + +Medusa defines a read-only link between the `Order` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of an order's region, but you don't manage the links in a pivot table in the database. The region of an order is determined by the `region_id` property of the `Order` data model. + +### Retrieve with Query + +To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders[0].region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", + ], +}) + +// orders[0].region +``` + +*** + +## Sales Channel Module + +Medusa defines a read-only link between the `Order` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of an order's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of an order is determined by the `sales_channel_id` property of the `Order` data model. + +### Retrieve with Query + +To retrieve the sales channel of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders[0].sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders[0].sales_channel +``` + + +# Order Change + +In this document, you'll learn about the Order Change data model and possible actions in it. + +## OrderChange Data Model + +The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. + +Its `change_type` property indicates what the order change is created for: + +1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). +2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). +3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). +4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +Once the order change is confirmed, its changes are applied on the order. + +*** + +## Order Change Actions + +The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). + +The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. + +The following table lists the possible `action` values that Medusa uses and what `details` they carry. + +|Action|Description|Details| +|---|---|---|---|---| +|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| +|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| +|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| +|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| +|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| + + +# Order Versioning + +In this document, you’ll learn how an order and its details are versioned. + +## What's Versioning? + +Versioning means assigning a version number to a record, such as an order and its items. This is useful to view the different versions of the order following changes in its lifetime. + +When changes are made on an order, such as an item is added or returned, the order's version changes. + +*** + +## version Property + +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. + +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. + +*** + +## How the Version Changes + +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: + +1. The version of the order and its summary is incremented. +2. Related order data that have a `version` property, such as the `OrderItem`, are duplicated. The duplicated item has the new version, whereas the original item has the previous version. + +When the order is retrieved, only the related data having the same version is retrieved. + + +# Order Claim + +In this document, you’ll learn about order claims. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. + +## What is a Claim? + +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. + +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. + +*** + +## Claim Type + +The `Claim` data model has a `type` property whose value indicates the type of the claim: + +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. + +*** + +## Old and Replacement Items + +When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. + +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). + +*** + +## Claim Shipping Methods + +A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). + +The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). + +*** + +## Claim Refund + +If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. + +The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. + +*** + +## How Claims Impact an Order’s Version + +When a claim is confirmed, the order’s version is incremented. + + +# Promotions Adjustments in Orders + +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +An adjustment line indicates a change to a line item or a shipping method’s amount. It’s used to apply promotions or discounts on an order. + +The [OrderLineItemAdjustment data model](https://docs.medusajs.com/references/order/models/OrderLineItemAdjustment/index.html.md) represents changes on a line item, and the [OrderShippingMethodAdjustment data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodAdjustment/index.html.md) represents changes on a shipping method. + +![A diagram showcasing the relation between an order, its items and shipping methods, and their adjustment lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712306017/Medusa%20Resources/order-adjustments_myflir.jpg) + +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. + +The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. + +*** + +## Discountable Option + +The `OrderLineItem` data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + +When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. + +*** + +## Promotion Actions + +When using the Order and Promotion modules together, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. + +Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). + +```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" + +// ... + +// retrieve the order +const order = await orderModuleService.retrieveOrder("ord_123", { + relations: [ + "items.item.adjustments", + "shipping_methods.shipping_method.adjustments", + ], +}) +// retrieve the line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +order.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + ...item.detail, + adjustments: filteredAdjustments, + }) + } +}) + +//retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +order.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + // TODO infer from cart or region + currency_code: "usd", + } +) +``` + +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. + +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the order’s line items and the shipping method’s adjustments. + +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" + +// ... + +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` + + +# Order Return + +In this document, you’ll learn about order returns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. + +## What is a Return? + +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). + +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. + +![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) + +Once the merchant receives the returned items, they mark the return as received. + +*** + +## Returned Items + +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). + +The `ReturnItem` model has two properties storing the item's quantity: + +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. + +*** + +## Return Shipping Methods + +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). + +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. + +*** + +## Refund Payment + +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. + +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. + +*** + +## Returns in Exchanges and Claims + +When a merchant creates an exchange or a claim, it includes returning items from the customer. + +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. + +*** + +## How Returns Impact an Order’s Version + +The order’s version is incremented when: + +1. A return is requested. +2. A return is marked as received. + + +# Tax Lines in Order Module + +In this document, you’ll learn about tax lines in an order. + +## What are Tax Lines? + +A tax line indicates the tax rate of a line item or a shipping method. + +The [OrderLineItemTaxLine data model](https://docs.medusajs.com/references/order/models/OrderLineItemTaxLine/index.html.md) represents a line item’s tax line, and the [OrderShippingMethodTaxLine data model](https://docs.medusajs.com/references/order/models/OrderShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + +![A diagram showcasing the relation between orders, items and shipping methods, and tax lines](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307225/Medusa%20Resources/order-tax-lines_sixujd.jpg) + +*** + +## Tax Inclusivity + +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount and then adding it to the item/method’s subtotal. + +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. + +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. + +The following diagram is a simplified showcase of how a subtotal is calculated from the tax perspective. + +![A diagram showcasing how a subtotal is calculated from the tax perspective](https://res.cloudinary.com/dza7lstvk/image/upload/v1712307395/Medusa%20Resources/order-tax-inclusive_oebdnm.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. + + +# Pricing Concepts + +In this document, you’ll learn about the main concepts in the Pricing Module. + +## Price Set + +A [PriceSet](https://docs.medusajs.com/references/pricing/models/PriceSet/index.html.md) represents a collection of prices that are linked to a resource (for example, a product or a shipping option). + +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +![A diagram showcasing the relation between the price set and price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648650/Medusa%20Resources/price-set-money-amount_xeees0.jpg) + +*** + +## Price List + +A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices only enabled if their conditions and rules are satisfied. + +A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied. + +Its associated prices are represented by the `Price` data model. + + +# Transactions + +In this document, you’ll learn about an order’s transactions and its use. + +## What is a Transaction? + +A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. + +Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. + +*** + +## Checking Outstanding Amount + +The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. + +```json +{ + "totals": { + "total": 30, + "subtotal": 30, + // ... + } +} +``` + +To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: + +|Condition|Result| +|---|---|---| +|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| +|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| +|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| + +*** + +## Transaction Reference + +The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). + +The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: + +- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. +- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. + + +# Price Tiers and Rules + +In this Pricing Module guide, you'll learn about tired prices, price rules for price sets and price lists, and how to add rules to a price. + +## Tiered Pricing + +Each price, represented by the [Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md), has two optional properties that can be used to create tiered prices: + +- `min_quantity`: The minimum quantity that must be in the cart for the price to be applied. +- `max_quantity`: The maximum quantity that can be in the cart for the price to be applied. + +This is useful to set tiered pricing for resources like product variants and shipping options. + +For example, you can set a variant's price to: + +- `$10` by default. +- `$8` when the customer adds `10` or more of the variant to the cart. +- `$6` when the customer adds `20` or more of the variant to the cart. + +These price definitions would look like this: + +```json title="Example Prices" +[ + // default price + { + "amount": 10, + "currency_code": "usd", + }, + { + "amount": 8, + "currency_code": "usd", + "min_quantity": 10, + "max_quantity": 19, + }, + { + "amount": 6, + "currency_code": "usd", + "min_quantity": 20, + }, +], +``` + +### How to Create Tiered Prices? + +When you create prices, you can specify a `min_quantity` and `max_quantity` for each price. This allows you to create tiered pricing, where the price changes based on the quantity of items in the cart. + +For example: + +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. + +### Using Medusa Workflows + +```ts highlights={tieredPricingHighlights} +const { result } = await createProductsWorkflow(container) + .run({ + input: { + products: [{ + variants: [{ + id: "variant_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, + ], + // ... + }] + }], + // ... + } + }) +``` + +### Using the Pricing Module + +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + amount: 10, + currency_code: "usd", + }, + // tiered prices + { + amount: 8, + currency_code: "usd", + min_quantity: 10, + max_quantity: 19, + }, + { + amount: 6, + currency_code: "usd", + min_quantity: 20, + }, + ], +}) +``` + +In this example, you create a product with a variant whose default price is `$10`. You also add two tiered prices that set the price to `$8` when the quantity is between `10` and `19`, and to `$6` when the quantity is `20` or more. + +### How are Tiered Prices Applied? + +The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers the cart's items as a context when choosing the best price to apply. + +For example, consider the customer added the `variant_1` product variant (created in the workflow snippet of the [above section](#how-to-create-tiered-prices)) to their cart with a quantity of `15`. + +The price calculation mechanism will choose the second price, which is `$8`, because the quantity of `15` is between `10` and `19`. + +If there are other rules applied to the price, they may affect the price calculation. Keep reading to learn about other price rules, and refer to the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide for more details on the calculation mechanism. + +*** + +## Price Rule + +You can also restrict prices by advanced rules, such as a customer's group, zip code, or a cart's total. + +Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). + +The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. + +For exmaple, you create a price restricted to `10557` zip codes. + +![A diagram showcasing the relation between the PriceRule and Price](https://res.cloudinary.com/dza7lstvk/image/upload/v1709648772/Medusa%20Resources/price-rule-1_vy8bn9.jpg) + +A price can have multiple price rules. + +For example, a price can be restricted by a region and a zip code. + +![A diagram showcasing the relation between the PriceRule and Price with multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709649296/Medusa%20Resources/price-rule-3_pwpocz.jpg) + +### Price List Rules + +Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). + +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. + +![A diagram showcasing the relation between the PriceSet, PriceList, Price, RuleType, and PriceListRuleValue](https://res.cloudinary.com/dza7lstvk/image/upload/v1709641999/Medusa%20Resources/price-list_zd10yd.jpg) + +### How to Create Prices with Rules? + +When you create prices, you can specify rules for each price. This allows you to create complex pricing strategies based on different contexts. + +For example: + +For most use cases where you're building customizations in the Medusa application, it's highly recommended to use [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) rather than using the Pricing Module directly. Medusa's workflows already implement extensive functionalities that you can re-use in your custom flows, with reliable roll-back mechanism. + +### Using Medusa Workflows + +```ts highlights={workflowHighlights} +const { result } = await createShippingOptionsWorkflow(container) + .run({ + input: [{ + name: "Standard Shipping", + service_zone_id: "serzo_123", + shipping_profile_id: "sp_123", + provider_id: "prov_123", + type: { + label: "Standard", + description: "Standard shipping", + code: "standard", + }, + price_type: "flat", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, + ], + }], + }) +``` + +### Using the Pricing Module + +```ts +const priceSet = await pricingModule.addPrices({ + priceSetId: "pset_1", + prices: [ + // default price + { + currency_code: "usd", + amount: 10, + rules: {}, + }, + // price if cart total >= $100 + { + currency_code: "usd", + amount: 0, + rules: { + item_total: { + operator: "gte", + value: 100, + }, + }, + }, + ], +}) +``` + +In this example, you create a shipping option whose default price is `$10`. When the total of the cart or order using this shipping option is greater than `$100`, the shipping option's price becomes free. + +### How is the Price Rule Applied? + +The [price calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) mechanism considers a price applicable when the resource that this price is in matches the specified rules. + +For example, a [cart object](https://docs.medusajs.com/api/store#carts_cart_schema) has an `item_total` property. So, if a shipping option has the following price: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "item_total": { + "operator": "gte", + "value": 100, + } + } +} +``` + +The shipping option's price is applied when the cart's `item_total` is greater than or equal to `$100`. + +You can also apply the rule on nested relations and properties. For example, to apply a shipping option's price based on the customer's group, you can apply a rule on the `customer.group.id` attribute: + +```json +{ + "currency_code": "usd", + "amount": 0, + "rules": { + "customer.group.id": { + "operator": "eq", + "value": "cusgrp_123" + } + } +} +``` + +In this example, the price is only applied if a cart's customer belongs to the customer group of ID `cusgrp_123`. + +These same rules apply to product variant prices as well, or any other resource that has a price. + + +# Prices Calculation + +In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. + +## calculatePrices Method + +The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context. + +It returns a price object with the best matching price for each price set. + +### Calculation Context + +The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set. + +For example: + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) +``` + +In this example, you retrieve the prices in a price set for the specified currency code and region ID. + +### Returned Price Object + +For each price set, the `calculatePrices` method selects two prices: + +- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price. +- An original price, which is either: + - The same price as the calculated price if the price list it belongs to is of type `override`; + - Or a price that doesn't belong to a price list and best matches the specified context. + +Both prices are returned in an object that has the following properties: + +- id: (\`string\`) The ID of the price set from which the price was selected. +- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list. +- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer. +- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list. +- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value. +- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price. +- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx) +- calculated\_price: (\`object\`) The calculated price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. + + - id: (\`string\`) The ID of the price. + + - price\_list\_id: (\`string\`) The ID of the associated price list. + + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. + + - min\_quantity: (\`number\`) The price's min quantity condition. + + - max\_quantity: (\`number\`) The price's max quantity condition. + +*** + +## Examples + +Consider the following price set: + +```ts +const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + // default price + { + amount: 500, + currency_code: "EUR", + rules: {}, + }, + // prices with rules + { + amount: 400, + currency_code: "EUR", + rules: { + region_id: "reg_123", + }, + }, + { + amount: 450, + currency_code: "EUR", + rules: { + city: "krakow", + }, + }, + { + amount: 500, + currency_code: "EUR", + rules: { + city: "warsaw", + region_id: "reg_123", + }, + }, + { + amount: 200, + currency_code: "EUR", + min_quantity: 100, + } + ], +}) +``` + +### Default Price Selection + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` + +### Result + +### Calculate Prices with Rules + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` + +### Result + +### Tiered Pricing Selection + +### Code + +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + cart: { + items: [ + { + id: "item_1", + quantity: 200, + // assuming the price set belongs to this variant + variant_id: "variant_1", + // ... + } + ], + // ... + } + } + } +) +``` + +### Result + +### Price Selection with Price List + +### Code + +```ts +const priceList = pricingModuleService.createPriceLists([{ + title: "Summer Price List", + description: "Price list for summer sale", + starts_at: Date.parse("01/10/2023").toString(), + ends_at: Date.parse("31/10/2023").toString(), + rules: { + region_id: ['PL'] + }, + type: "sale", + prices: [ + { + amount: 400, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + { + amount: 450, + currency_code: "EUR", + price_set_id: priceSet.id, + }, + ], +}]); + +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` + +### Result + + +# Links between Pricing Module and Other Modules + +This document showcases the module links defined between the Pricing Module and other Commerce Modules. + +## Summary + +The Pricing Module has the following links to other modules: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ShippingOption|PriceSet|Stored - one-to-one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| + +*** + +## Fulfillment Module + +The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. + +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +### Retrieve with Query + +To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: + +### query.graph + +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) + +// priceSets[0].shipping_option +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) + +// priceSets[0].shipping_option +``` + +### Manage with Link + +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Product Module + +The Product Module doesn't store or manage the prices of product variants. + +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. + +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) + +So, when you want to add prices for a product variant, you create a price set and add the prices to it. + +You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. + +### Retrieve with Query + +To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +### query.graph + +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets[0].variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets[0].variant +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + + +# Tax-Inclusive Pricing + +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. + +## What is Tax-Inclusive Pricing? + +A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. + +For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. + +*** + +## How is Tax-Inclusive Pricing Set? + +The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: + +- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. +- `value`: The attribute’s value. For example, `reg_123` or `usd`. + +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. + +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. + +For example: + +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` + +In this example, tax-inclusivity is enabled for the `USD` currency code. + +*** + +## Tax-Inclusive Pricing in Price Calculation + +### Tax Context + +As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. + +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. + +### Returned Tax Properties + +The `calculatePrices` method returns two properties related to tax-inclusivity: + +Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). + +- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. +- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. + +A price is considered tax-inclusive if: + +1. It belongs to the region or currency code specified in the calculation context; +2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. + +### Tax Context Precedence + +A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: + +- both the `region_id` and `currency_code` are provided in the calculation context; +- the selected price belongs to the region; +- and the region has a price preference + + # Links between Region Module and Other Modules This document showcases the module links defined between the Region Module and other Commerce Modules. @@ -27953,9 +28472,9 @@ Read-only links are used to query data across modules, but the relations aren't |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Read-only - has one|| -| in ||Read-only - has one|| -|| in |Stored - many-to-many|| +|Cart|Region|Read-only - has one|Learn more| +|Order|Region|Read-only - has one|Learn more| +|Region|PaymentProvider|Stored - many-to-many|Learn more| *** @@ -28121,385 +28640,43 @@ createRemoteLinkStep({ ``` -# Promotion Actions +# Links between Store Module and Other Modules -In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). - -## computeActions Method - -The Promotion Module's main service has a [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md) that returns an array of actions to perform on a cart when one or more promotions are applied. - -Actions inform you what adjustment must be made to a cart item or shipping method. Each action is an object having the `action` property indicating the type of action. - -*** - -## Action Types - -### `addItemAdjustment` Action - -The `addItemAdjustment` action indicates that an adjustment must be made to an item. For example, removing $5 off its amount. - -This action has the following format: - -```ts -export interface AddItemAdjustmentAction { - action: "addItemAdjustment" - item_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `LineItemAdjustment` data model in the Cart Module, or `OrderLineItemAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `removeItemAdjustment` Action - -The `removeItemAdjustment` action indicates that an adjustment must be removed from a line item. For example, remove the $5 discount. - -The `computeActions` method accepts any previous item adjustments in the `items` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveItemAdjustmentAction { - action: "removeItemAdjustment" - adjustment_id: string - description?: string - code: string -} -``` - -This action means that a new record should be removed of the `LineItemAdjustment` (or `OrderLineItemAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveItemAdjustmentAction/index.html.md) for details on the object’s properties. - -### `addShippingMethodAdjustment` Action - -The `addShippingMethodAdjustment` action indicates that an adjustment must be made on a shipping method. For example, make the shipping method free. - -This action has the following format: - -```ts -export interface AddShippingMethodAdjustment { - action: "addShippingMethodAdjustment" - shipping_method_id: string - amount: number - code: string - description?: string -} -``` - -This action means that a new record should be created of the `ShippingMethodAdjustment` data model in the Cart Module, or `OrderShippingMethodAdjustment` data model in the Order Module. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.AddShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `removeShippingMethodAdjustment` Action - -The `removeShippingMethodAdjustment` action indicates that an adjustment must be removed from a shipping method. For example, remove the free shipping discount. - -The `computeActions` method accepts any previous shipping method adjustments in the `shipping_methods` property of the second parameter. - -This action has the following format: - -```ts -export interface RemoveShippingMethodAdjustment { - action: "removeShippingMethodAdjustment" - adjustment_id: string - code: string -} -``` - -When the Medusa application receives this action type, it removes the `ShippingMethodAdjustment` (or `OrderShippingMethodAdjustment`) with the specified ID in the `adjustment_id` property. - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.RemoveShippingMethodAdjustment/index.html.md) for details on the object’s properties. - -### `campaignBudgetExceeded` Action - -When the `campaignBudgetExceeded` action is returned, the promotions within a campaign can no longer be used as the campaign budget has been exceeded. - -This action has the following format: - -```ts -export interface CampaignBudgetExceededAction { - action: "campaignBudgetExceeded" - code: string -} -``` - -Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. - - -# Application Method - -In this document, you'll learn what an application method is. - -## What is an Application Method? - -The [ApplicationMethod data model](https://docs.medusajs.com/references/promotion/models/ApplicationMethod/index.html.md) defines how a promotion is applied: - -|Property|Purpose| -|---|---| -|\`type\`|Does the promotion discount a fixed amount or a percentage?| -|\`target\_type\`|Is the promotion applied on a cart item, shipping method, or the entire order?| -|\`allocation\`|Is the discounted amount applied on each item or split between the applicable items?| - -## Target Promotion Rules - -When the promotion is applied to a cart item or a shipping method, you can restrict which items/shipping methods the promotion is applied to. - -The `ApplicationMethod` data model has a collection of `PromotionRule` records to restrict which items or shipping methods the promotion applies to. The `target_rules` property represents this relation. - -![A diagram showcasing the target\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898273/Medusa%20Resources/application-method-target-rules_hqaymz.jpg) - -In this example, the promotion is only applied on products in the cart having the SKU `SHIRT`. - -*** - -## Buy Promotion Rules - -When the promotion’s type is `buyget`, you must specify the “buy X” condition. For example, a cart must have two shirts before the promotion can be applied. - -The application method has a collection of `PromotionRule` items to define the “buy X” rule. The `buy_rules` property represents this relation. - -![A diagram showcasing the buy\_rules relation between the ApplicationMethod and PromotionRule data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709898453/Medusa%20Resources/application-method-buy-rules_djjuhw.jpg) - -In this example, the cart must have two products with the SKU `SHIRT` for the promotion to be applied. - - -# Campaign - -In this document, you'll learn about campaigns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. - -## What is a Campaign? - -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. - -![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) - -*** - -## Campaign Limits - -Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. - -There are two types of budgets: - -- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. -- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. - -![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) - - -# Promotion Concepts - -In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. - -## What is a Promotion? - -A promotion, represented by the [Promotion data model](https://docs.medusajs.com/references/promotion/models/Promotion/index.html.md), is a discount that can be applied on cart items, shipping methods, or entire orders. - -A promotion has two types: - -- `standard`: A standard promotion with rules. -- `buyget`: “A buy X get Y” promotion with rules. - -|\`standard\`|\`buyget\`| -|---|---| -|A coupon code that gives customers 10% off their entire order.|Buy two shirts and get another for free.| -|A coupon code that gives customers $15 off any shirt in their order.|Buy two shirts and get 10% off the entire order.| -|A discount applied automatically for VIP customers that removes 10% off their shipping method’s amount.|Spend $100 and get free shipping.| - -The Medusa Admin UI may not provide a way to create each of these promotion examples. However, they are supported by the Promotion Module and Medusa's workflows and API routes. - -*** - -## Promotion Rules - -A promotion can be restricted by a set of rules, each rule is represented by the [PromotionRule data model](https://docs.medusajs.com/references/promotion/models/PromotionRule/index.html.md). - -For example, you can create a promotion that only customers of the `VIP` customer group can use. - -![A diagram showcasing the relation between Promotion and PromotionRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1709833196/Medusa%20Resources/promotion-promotion-rule_msbx0w.jpg) - -A `PromotionRule`'s `attribute` property indicates the property's name to which this rule is applied. For example, `customer_group_id`. - -The expected value for the attribute is stored in the `PromotionRuleValue` data model. So, a rule can have multiple values. - -When testing whether a promotion can be applied to a cart, the rule's `attribute` property and its values are tested on the cart itself. - -For example, the cart's customer must be part of the customer group(s) indicated in the promotion rule's value. - -### Flexible Rules - -The `PromotionRule`'s `operator` property adds more flexibility to the rule’s condition rather than simple equality (`eq`). - -For example, to restrict the promotion to only `VIP` and `B2B` customer groups: - -- Add a `PromotionRule` record with its `attribute` property set to `customer_group_id` and `operator` property to `in`. -- Add two `PromotionRuleValue` records associated with the rule: one with the value `VIP` and the other `B2B`. - -![A diagram showcasing the relation between PromotionRule and PromotionRuleValue when a rule has multiple values](https://res.cloudinary.com/dza7lstvk/image/upload/v1709897383/Medusa%20Resources/promotion-promotion-rule-multiple_hctpmt.jpg) - -In this case, a customer’s group must be in the `VIP` and `B2B` set of values to use the promotion. - -*** - -## How to Apply Rules on a Promotion? - -### Using Workflows - -If you're managing promotions using [Medusa's workflows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-workflows-reference/index.html.md) or the API routes that use them, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md). - -For example, if you're creating a promotion using the [createPromotionsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createPromotionsWorkflow/index.html.md): - -```ts -const { result } = await createPromotionsWorkflow(container) - .run({ - input: { - promotionsData: [{ - code: "10OFF", - type: "standard", - status: "active", - application_method: { - type: "percentage", - target_type: "items", - allocation: "across", - value: 10, - currency_code: "usd", - }, - rules: [ - { - attribute: "customer.group.id", - operator: "eq", - values: [ - "cusgrp_123", - ], - }, - ], - }], - }, - }) -``` - -In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. - -### Using Promotion Module's Service - -For most use cases, it's recommended to use [workflows](#using-workflows) instead of directly using the module's service. - -If you're managing promotions using the Promotion Module's service, you can specify rules for the promotion or its [application method](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/application-method/index.html.md) in its methods. - -For example, if you're creating a promotion with the [createPromotions](https://docs.medusajs.com/resources/references/promotion/createPromotions/index.html.md) method: - -```ts -const promotions = await promotionModuleService.createPromotions([ - { - code: "50OFF", - type: "standard", - status: "active", - application_method: { - type: "percentage", - target_type: "items", - value: 50, - }, - rules: [ - { - attribute: "customer.group.id", - operator: "eq", - values: [ - "cusgrp_123", - ], - }, - ], - }, -]) -``` - -In this example, the promotion is restricted to customers with the `cusgrp_123` customer group. - -### How is the Promotion Rule Applied? - -A promotion is applied on a resource if its attributes match the promotion's rules. - -For example, consider you have the following promotion with a rule that restricts the promotion to a specific customer: - -```json -{ - "code": "10OFF", - "type": "standard", - "status": "active", - "application_method": { - "type": "percentage", - "target_type": "items", - "allocation": "across", - "value": 10, - "currency_code": "usd" - }, - "rules": [ - { - "attribute": "customer_id", - "operator": "eq", - "values": [ - "cus_123" - ] - } - ] -} -``` - -When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`. - - -# Links between Promotion Module and Other Modules - -This document showcases the module links defined between the Promotion Module and other Commerce Modules. +This document showcases the module links defined between the Store Module and other Commerce Modules. ## Summary -The Promotion Module has the following links to other modules: +The Store Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Stored - many-to-many|| +|StoreCurrency|Currency|Read-only - has many|Learn more| *** -## Cart Module +## Currency Module -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. +Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). ### Retrieve with Query -To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: ### query.graph ```ts -const { data: promotions } = await query.graph({ - entity: "promotion", +const { data: stores } = await query.graph({ + entity: "store", fields: [ - "carts.*", + "supported_currencies.currency.*", ], }) -// promotions[0].carts +// stores[0].supported_currencies ``` ### useQueryGraphStep @@ -28509,19 +28686,277 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: stores } = useQueryGraphStep({ + entity: "store", fields: [ - "carts.*", + "supported_currencies.currency.*", ], }) -// promotions[0].carts +// stores[0].supported_currencies +``` + + +# Tax Calculation with the Tax Provider + +In this document, you’ll learn how tax lines are calculated and what a tax provider is. + +## Tax Lines Calculation + +Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. + +For example: + +```ts +const taxLines = await taxModuleService.getTaxLines( + [ + { + id: "cali_123", + product_id: "prod_123", + unit_price: 1000, + quantity: 1, + }, + { + id: "casm_123", + shipping_option_id: "so_123", + unit_price: 2000, + }, + ], + { + address: { + country_code: "us", + }, + } +) +``` + +The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. + +The example above retrieves the tax lines based on the tax region for the United States. + +The method returns tax lines for the line item and shipping methods. For example: + +```json +[ + { + "line_item_id": "cali_123", + "rate_id": "txr_1", + "rate": 10, + "code": "XXX", + "name": "Tax Rate 1" + }, + { + "shipping_line_id": "casm_123", + "rate_id": "txr_2", + "rate": 5, + "code": "YYY", + "name": "Tax Rate 2" + } +] +``` + +*** + +## Using the Tax Provider in the Calculation + +The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. + +A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. + +The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. + +{/* --- + +TODO add once tax provider guide is updated + add module providers match other modules. + +## Create Tax Provider + +Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} + + +# Tax Module Options + +In this document, you'll learn about the options of the Tax Module. + +## providers + +The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. + +When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The objects in the array accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Tax Rates and Rules + +In this document, you’ll learn about tax rates and rules. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. + +## What are Tax Rates? + +A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. + +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. + +### Combinable Tax Rates + +Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. + +Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. + +*** + +## Override Tax Rates with Rules + +You can create tax rates that override the default for specific conditions or rules. + +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. + +A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. + +![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) + +These two properties of the data model identify the rule’s target: + +- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. +- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. + +So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. + + +# Tax Region + +In this document, you’ll learn about tax regions and how to use them with the Region Module. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. + +## What is a Tax Region? + +A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. + +Tax regions can inherit settings and rules from a parent tax region. + +Each tax region has tax rules and a tax provider. + + +# Stock Location Concepts + +In this document, you’ll learn about the main concepts in the Stock Location Module. + +## Stock Location + +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. + +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. + +*** + +## StockLocationAddress + +The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. + + +# Links between Stock Location Module and Other Modules + +This document showcases the module links defined between the Stock Location Module and other Commerce Modules. + +## Summary + +The Stock Location Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more| +|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more| +|InventoryLevel|StockLocation|Read-only - has many|Learn more| +|SalesChannel|StockLocation|Stored - many-to-many|Learn more| + +*** + +## Fulfillment Module + +A fulfillment set can be conditioned to a specific stock location. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: + +To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations[0].fulfillment_sets +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations[0].fulfillment_sets ``` ### Manage with Link -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -28531,11 +28966,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` @@ -28549,38 +28984,36 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` *** -## Order Module +## Inventory Module -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. - -![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) +Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. ### Retrieve with Query -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: ### query.graph ```ts -const { data: promotions } = await query.graph({ - entity: "promotion", +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", fields: [ - "orders.*", + "stock_locations.*", ], }) -// promotions[0].orders +// inventoryLevels[0].stock_locations ``` ### useQueryGraphStep @@ -28590,19 +29023,63 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", fields: [ - "orders.*", + "stock_locations.*", ], }) -// promotions[0].orders +// inventoryLevels[0].stock_locations +``` + +*** + +## Sales Channel Module + +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels ``` ### Manage with Link -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -28612,11 +29089,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` @@ -28630,11 +29107,11 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", }, }) ``` @@ -28707,11 +29184,11 @@ Read-only links are used to query data across modules, but the relations aren't |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -| in ||Stored - many-to-many|| -| in ||Read-only - has one|| -| in ||Read-only - has one|| -| in ||Stored - many-to-many|| -|| in |Stored - many-to-many|| +|ApiKey|SalesChannel|Stored - many-to-many|Learn more| +|Cart|SalesChannel|Read-only - has one|Learn more| +|Order|SalesChannel|Read-only - has one|Learn more| +|Product|SalesChannel|Stored - many-to-many|Learn more| +|SalesChannel|StockLocation|Stored - many-to-many|Learn more| *** @@ -29043,347 +29520,6 @@ createRemoteLinkStep({ ``` -# Stock Location Concepts - -In this document, you’ll learn about the main concepts in the Stock Location Module. - -## Stock Location - -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. - -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. - -*** - -## StockLocationAddress - -The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. - - -# Links between Stock Location Module and Other Modules - -This document showcases the module links defined between the Stock Location Module and other Commerce Modules. - -## Summary - -The Stock Location Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -| in ||Stored - many-to-one|| -| in ||Stored - many-to-many|| -| in ||Read-only - has many|| -| in ||Stored - many-to-many|| - -*** - -## Fulfillment Module - -A fulfillment set can be conditioned to a specific stock location. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: - -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) - -// stockLocations[0].fulfillment_sets -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) - -// stockLocations[0].fulfillment_sets -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -*** - -## Inventory Module - -Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. - -### Retrieve with Query - -To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: - -### query.graph - -```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels[0].stock_locations -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) - -// inventoryLevels[0].stock_locations -``` - -*** - -## Sales Channel Module - -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. - -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) - -### Retrieve with Query - -To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations[0].sales_channels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) - -// stockLocations[0].sales_channels -``` - -### Manage with Link - -To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` - - -# Links between Store Module and Other Modules - -This document showcases the module links defined between the Store Module and other Commerce Modules. - -## Summary - -The Store Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|| in |Read-only - has many|| - -*** - -## Currency Module - -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. - -Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). - -### Retrieve with Query - -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: - -### query.graph - -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies -``` - - -# User Module Options - -In this document, you'll learn about the options of the User Module. - -## Module Options - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], -}) -``` - -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| - -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```bash -JWT_SECRET=supersecret -``` - - # User Creation Flows In this document, learn the different ways to create a user using the User Module. @@ -29464,15 +29600,11 @@ if (!count) { ``` -# Tax Module Options +# User Module Options -In this document, you'll learn about the options of the Tax Module. +In this document, you'll learn about the options of the User Module. -## providers - -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. - -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. +## Module Options ```ts title="medusa-config.ts" import { Modules } from "@medusajs/framework/utils" @@ -29483,173 +29615,46 @@ module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/tax", + resolve: "@medusajs/user", options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], + jwt_secret: process.env.JWT_SECRET, }, }, ], }) ``` -The objects in the array accept the following properties: +|Option|Description|Required| +|---|---|---|---|---| +|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| -- `resolve`: A string indicating the package name of the module provider or the path to it. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. +### Environment Variables +Make sure to add the necessary environment variables for the above options in `.env`: -# Tax Calculation with the Tax Provider - -In this document, you’ll learn how tax lines are calculated and what a tax provider is. - -## Tax Lines Calculation - -Tax lines are calculated and retrieved using the [getTaxLines method of the Tax Module’s main service](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). It accepts an array of line items and shipping methods, and the context of the calculation. - -For example: - -```ts -const taxLines = await taxModuleService.getTaxLines( - [ - { - id: "cali_123", - product_id: "prod_123", - unit_price: 1000, - quantity: 1, - }, - { - id: "casm_123", - shipping_option_id: "so_123", - unit_price: 2000, - }, - ], - { - address: { - country_code: "us", - }, - } -) +```bash +JWT_SECRET=supersecret ``` -The context object is used to determine which tax regions and rates to use in the calculation. It includes properties related to the address and customer. -The example above retrieves the tax lines based on the tax region for the United States. +# GitHub Auth Module Provider -The method returns tax lines for the line item and shipping methods. For example: +In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. -```json -[ - { - "line_item_id": "cali_123", - "rate_id": "txr_1", - "rate": 10, - "code": "XXX", - "name": "Tax Rate 1" - }, - { - "shipping_line_id": "casm_123", - "rate_id": "txr_2", - "rate": 5, - "code": "YYY", - "name": "Tax Rate 2" - } -] -``` +The Github Auth Module Provider authenticates users with their GitHub account. + +Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). *** -## Using the Tax Provider in the Calculation +## Register the Github Auth Module Provider -The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s provider. +### Prerequisites -A tax module provider whose main service implements the logic to shape tax lines. Each tax region has a tax provider. +- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) +- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) -The Tax Module provides a `system` tax provider that only transforms calculated item and shipping tax rates into the required return type. - -{/* --- - -TODO add once tax provider guide is updated + add module providers match other modules. - -## Create Tax Provider - -Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} - - -# Tax Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -Each tax region has tax rules and a tax provider. - - -# Emailpass Auth Module Provider - -In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. - -Using the Emailpass auth module provider, you allow users to register and login with an email and password. - -*** - -## Register the Emailpass Auth Module Provider - -The Emailpass auth provider is registered by default with the Auth Module. - -If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: +Add the module to the array of providers passed to the Auth Module: ```ts title="medusa-config.ts" import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" @@ -29666,10 +29671,12 @@ module.exports = defineConfig({ providers: [ // other providers... { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", + resolve: "@medusajs/medusa/auth-github", + id: "github", options: { - // options... + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GITHUB_CALLBACK_URL, }, }, ], @@ -29679,24 +29686,37 @@ module.exports = defineConfig({ }) ``` +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= +``` + ### Module Options -|Configuration|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`hashConfig\`|An object of configurations to use when hashing the user's -password. Refer to |No|\`\`\`ts -const hashConfig = \{ - logN: 15, - r: 8, - p: 1 -} -\`\`\`| +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| +|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| *** -## Related Guides +## Override Callback URL During Authentication -- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. + +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). + +*** + +## Examples + +- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). # Google Auth Module Provider @@ -29786,24 +29806,19 @@ The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednass - [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). -# GitHub Auth Module Provider +# Emailpass Auth Module Provider -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. -The Github Auth Module Provider authenticates users with their GitHub account. - -Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +Using the Emailpass auth module provider, you allow users to register and login with an email and password. *** -## Register the Github Auth Module Provider +## Register the Emailpass Auth Module Provider -### Prerequisites +The Emailpass auth provider is registered by default with the Auth Module. -- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) -- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) - -Add the module to the array of providers passed to the Auth Module: +If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: ```ts title="medusa-config.ts" import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" @@ -29820,12 +29835,10 @@ module.exports = defineConfig({ providers: [ // other providers... { - resolve: "@medusajs/medusa/auth-github", - id: "github", + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", options: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: process.env.GITHUB_CALLBACK_URL, + // options... }, }, ], @@ -29835,37 +29848,24 @@ module.exports = defineConfig({ }) ``` -### Environment Variables - -Make sure to add the necessary environment variables for the above options in `.env`: - -```plain -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_CALLBACK_URL= -``` - ### Module Options -|Configuration|Description|Required| -|---|---|---|---|---| -|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| -|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| +|Configuration|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`hashConfig\`|An object of configurations to use when hashing the user's +password. Refer to |No|\`\`\`ts +const hashConfig = \{ + logN: 15, + r: 8, + p: 1 +} +\`\`\`| *** -## Override Callback URL During Authentication +## Related Guides -In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. - -The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). - -*** - -## Examples - -- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). +- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) # Stripe Module Provider @@ -30001,84 +30001,157 @@ When you set up the webhook in Stripe, choose the following events to listen to: - [Customize Stripe Integration in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/guides/customize-stripe/index.html.md). -# Get Product Variant Prices using Query +# Get Product Variant Inventory Quantity -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. +Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide. -So, to retrieve data across the linked records of the two modules, you use Query. +## Understanding Product Variant Inventory Availability -## Retrieve All Product Variant Prices +Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant. -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. +When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products. -For example: +When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) -``` - -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). +This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled. *** -## Retrieve Calculated Price for a Context +## Retrieve Product Variant Inventory -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: - -- Pass `variants.calculated_price.*` in the `fields` property. -- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. +To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant. For example: -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" +```ts highlights={variantAvailabilityHighlights} +import { getVariantAvailability } from "@medusajs/framework/utils" // ... -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, +// use req.scope instead of container in API routes +const query = container.resolve("query") + +const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: "sc_123", }) ``` -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. +A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md). -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). +So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in. -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). +Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function. + +### Parameters + +The `getVariantAvailability` function accepts the following parameters: + +- query: (Query) Instance of Query to retrieve the necessary data. +- options: (\`object\`) The options to retrieve the variant availability. + + - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability. + + - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in. + +### Returns + +The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter. + +The value of each key is an object with the following properties: + +- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`. +- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to. + +For example, the object may look like this: + +```json title="Example result" +{ + "variant_123": { + "availability": 10, + "sales_channel_id": "sc_123" + } +} +``` + +*** + +## Retrieve Sales Channel to Use + +To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either: + +- Use the sales channel of the request's scope. +- Use the sales channel that the variant's product is available in. + +### Method 1: Use Sales Channel Scope in Store Routes + +Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key. + +So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object: + +```ts highlights={salesChannelScopeHighlights} +import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" +import { getVariantAvailability } from "@medusajs/framework/utils" + +export async function GET( + req: MedusaStoreRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") + const sales_channel_ids = req.publishable_key_context.sales_channel_ids + + const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: sales_channel_ids[0], + }) + + res.json({ + availability, + }) +} +``` + +In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. + +Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. + +Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. + +### Method 2: Use Product's Sales Channel + +A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels. + +For example: + +```ts highlights={productSalesChannelHighlights} +import { getVariantAvailability } from "@medusajs/framework/utils" + +// ... + +// use req.scope instead of container in API routes +const query = container.resolve("query") + +const { data: variants } = await query.graph({ + entity: "variant", + fields: ["id", "product.sales_channels.*"], + filters: { + id: "variant_123", + }, +}) + +const availability = await getVariantAvailability(query, { + variant_ids: ["variant_123"], + sales_channel_id: variants[0].product!.sales_channels![0]!.id, +}) +``` + +In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product. + +Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. # Calculate Product Variant Price with Taxes @@ -30266,780 +30339,2006 @@ For each product variant, you: - `priceWithoutTax`: The variant's price without taxes applied. -# Get Product Variant Inventory Quantity +# Get Product Variant Prices using Query -In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). -Refer to the [Retrieve Product Variant Inventory](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/products/inventory/index.html.md) storefront guide. +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. -## Understanding Product Variant Inventory Availability +So, to retrieve data across the linked records of the two modules, you use Query. -Product variants have a `manage_inventory` boolean field that indicates whether the Medusa application manages the inventory of the product variant. +## Retrieve All Product Variant Prices -When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. So, you can't retrieve the inventory quantity for those products. - -When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. - -This guide explains how to retrieve the inventory quantity of a product variant when `manage_inventory` is enabled. - -*** - -## Retrieve Product Variant Inventory - -To retrieve the inventory quantity of a product variant, use the `getVariantAvailability` utility function imported from `@medusajs/framework/utils`. It returns the available quantity of the product variant. +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. For example: -```ts highlights={variantAvailabilityHighlights} -import { getVariantAvailability } from "@medusajs/framework/utils" - -// ... - -// use req.scope instead of container in API routes -const query = container.resolve("query") - -const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: "sc_123", -}) -``` - -A product variant's inventory quantity is set per [stock location](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). This stock location is linked to a [sales channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md). - -So, to retrieve the inventory quantity of a product variant using `getVariantAvailability`, you need to also provide the ID of the sales channel to retrieve the inventory quantity in. - -Refer to the [Retrieve Sales Channel to Use](#retrieve-sales-channel-to-use) section to learn how to retrieve the sales channel ID to use in the `getVariantAvailability` function. - -### Parameters - -The `getVariantAvailability` function accepts the following parameters: - -- query: (Query) Instance of Query to retrieve the necessary data. -- options: (\`object\`) The options to retrieve the variant availability. - - - variant\_ids: (\`string\[]\`) The IDs of the product variants to retrieve their inventory availability. - - - sales\_channel\_id: (\`string\`) The ID of the sales channel to retrieve the variant availability in. - -### Returns - -The `getVariantAvailability` function resolves to an object whose keys are the IDs of each product variant passed in the `variant_ids` parameter. - -The value of each key is an object with the following properties: - -- availability: (\`number\`) The available quantity of the product variant in the stock location linked to the sales channel. If \`manage\_inventory\` is disabled, this value is \`0\`. -- sales\_channel\_id: (\`string\`) The ID of the sales channel that the availability is scoped to. - -For example, the object may look like this: - -```json title="Example result" -{ - "variant_123": { - "availability": 10, - "sales_channel_id": "sc_123" - } -} -``` - -*** - -## Retrieve Sales Channel to Use - -To retrieve the sales channel ID to use in the `getVariantAvailability` function, you can either: - -- Use the sales channel of the request's scope. -- Use the sales channel that the variant's product is available in. - -### Method 1: Use Sales Channel Scope in Store Routes - -Requests sent to API routes starting with `/store` must include a [publishable API key in the request header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). This scopes the request to one or more sales channels associated with the publishable API key. - -So, if you're retrieving the variant inventory availability in an API route starting with `/store`, you can access the sales channel using the `publishable_key_context.sales_channel_ids` property of the request object: - -```ts highlights={salesChannelScopeHighlights} -import { MedusaStoreRequest, MedusaResponse } from "@medusajs/framework/http" -import { getVariantAvailability } from "@medusajs/framework/utils" - -export async function GET( - req: MedusaStoreRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") - const sales_channel_ids = req.publishable_key_context.sales_channel_ids - - const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: sales_channel_ids[0], - }) - - res.json({ - availability, - }) -} -``` - -In this example, you retrieve the scope's sales channel IDs using `req.publishable_key_context.sales_channel_ids`, whose value is an array of IDs. - -Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. - -Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. - -### Method 2: Use Product's Sales Channel - -A product is linked to the sales channels it's available in. So, you can retrieve the details of the variant's product, including its sales channels. - -For example: - -```ts highlights={productSalesChannelHighlights} -import { getVariantAvailability } from "@medusajs/framework/utils" - -// ... - -// use req.scope instead of container in API routes -const query = container.resolve("query") - -const { data: variants } = await query.graph({ - entity: "variant", - fields: ["id", "product.sales_channels.*"], +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", + ], filters: { - id: "variant_123", + id: [ + "prod_123", + ], }, }) +``` -const availability = await getVariantAvailability(query, { - variant_ids: ["variant_123"], - sales_channel_id: variants[0].product!.sales_channels![0]!.id, +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). + +*** + +## Retrieve Calculated Price for a Context + +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. + +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). + +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: + +- Pass `variants.calculated_price.*` in the `fields` property. +- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. + +For example: + +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } from "@medusajs/framework/utils" + +// ... + +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", + ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, + }, }) ``` -In this example, you retrieve the sales channels of the variant's product using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. -You pass the ID of the variant as a filter, and you specify `product.sales_channels.*` as the fields to retrieve. This retrieves the sales channels linked to the variant's product. +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). -Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). ## Workflows -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) -- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) -- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) - [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) -- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) - [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) +- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) - [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) +- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) - [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) - [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) - [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) - [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) -- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) - [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/index.html.md) - [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) -- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) +- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) +- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) +- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) +- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) +- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) -- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) - [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) - [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) -- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) - [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) +- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) - [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) - [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) - [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) +- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) - [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md) - [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md) - [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) - [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) -- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) -- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) - [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) -- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) +- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) - [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) +- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) +- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) +- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) - [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) +- [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) +- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) +- [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) +- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) +- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) +- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) +- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) +- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) - [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) - [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) -- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) -- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) +- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) +- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) - [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) +- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) - [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md) -- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) -- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) - [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) +- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) +- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) +- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) - [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) -- [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) - [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) - [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) - [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) -- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) -- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) -- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) -- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) -- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) -- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) -- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) -- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) -- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) -- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) -- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) -- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) -- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) -- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) -- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) -- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) -- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) -- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) -- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) -- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) -- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) -- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) -- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) -- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) -- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) -- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) -- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) -- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) -- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) -- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) -- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) -- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) -- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) -- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) -- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) -- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) -- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) -- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) -- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) -- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) -- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) -- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) -- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) -- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) -- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) -- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) -- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) -- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) -- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) -- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) -- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) -- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) -- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) -- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) -- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) -- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) -- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) -- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) -- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) -- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) -- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) -- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) -- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) -- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) -- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) -- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) -- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) -- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) -- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) -- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) -- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) -- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) - [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) - [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) - [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) - [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) +- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) - [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) - [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) -- [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) - [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) +- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) +- [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) +- [removePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/removePriceListPricesWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [createPricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPricePreferencesWorkflow/index.html.md) +- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/index.html.md) +- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/index.html.md) +- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) +- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) +- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) +- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) +- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) +- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) +- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) +- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) +- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) +- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) +- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) +- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) +- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) +- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) +- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) +- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) +- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) +- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) +- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) +- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) +- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) +- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) +- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) +- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) +- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) +- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) +- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) +- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) +- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) +- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) +- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) +- [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) +- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) +- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) +- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) +- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) +- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) +- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) +- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) +- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) +- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) +- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) +- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) +- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) +- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) +- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) +- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) +- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) +- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) +- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) +- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/index.html.md) +- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) +- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) +- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) +- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) +- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) +- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) +- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) +- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) +- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) +- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) +- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) +- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) +- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) +- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) +- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) +- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) +- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) +- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) +- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) +- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) +- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) +- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) +- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) +- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) +- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) +- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) +- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) +- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) +- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) +- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) +- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) +- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) +- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) +- [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) - [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) - [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/index.html.md) -- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) -- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) +- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) - [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) - [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) +- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) - [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) - [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) -- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) -- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) - [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) - [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) - [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) -- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) -- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) -- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) -- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) - [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) -- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) -- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) -- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) -- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) -- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) +- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) - [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) -- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) - [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) - [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) +- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) +- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) +- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) +- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) +- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) - [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) - [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) -- [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) - [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) - [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) -- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) -- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) -- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) +- [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) +- [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) +- [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) +- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) +- [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/index.html.md) +- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) ## Steps -- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) -- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) - [linkSalesChannelsToApiKeyStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkSalesChannelsToApiKeyStep/index.html.md) - [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) -- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) +- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/index.html.md) +- [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) - [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) +- [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/index.html.md) - [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) -- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) -- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) -- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) -- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) -- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) -- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) -- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) -- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) -- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) -- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) -- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) -- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) -- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) -- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) -- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md) -- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) -- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) -- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) -- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) -- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) -- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) -- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) -- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) -- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) -- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) -- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) -- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) -- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) -- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) -- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) -- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) -- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) -- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) -- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) -- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) -- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) -- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) -- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) -- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) -- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) -- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) -- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) -- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) -- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) - [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) - [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) - [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) - [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) - [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) - [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) -- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) - [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) -- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) +- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) +- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) +- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) +- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) +- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) +- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) +- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) +- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) +- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) +- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) +- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) +- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) +- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) +- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) +- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) +- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) +- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) +- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/index.html.md) +- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) +- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) +- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) +- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) +- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) +- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) +- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) +- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) +- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) +- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) +- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) +- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md) +- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) +- [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) +- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) +- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) +- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) +- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) +- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) +- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) +- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) +- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) +- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) +- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) +- [setShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/setShippingOptionsPricesStep/index.html.md) +- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) +- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) +- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) +- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) +- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) +- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) +- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) +- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) +- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) +- [adjustInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/adjustInventoryLevelsStep/index.html.md) +- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) +- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) +- [createInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryItemsStep/index.html.md) +- [deleteInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryLevelsStep/index.html.md) +- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) +- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/index.html.md) +- [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) - [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) - [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/index.html.md) -- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) -- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) -- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) -- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) -- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) -- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) -- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) -- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) -- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) -- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) -- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) -- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) -- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) -- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) -- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) -- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) -- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) -- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) -- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) -- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) -- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) -- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) -- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) -- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) -- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) -- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) -- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) -- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) -- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) -- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) -- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) -- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) -- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) -- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) -- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) -- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) -- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) -- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) -- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) -- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) -- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) -- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) -- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) -- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) -- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) -- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) -- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) -- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) -- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) -- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) -- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) -- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) -- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) -- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) -- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) -- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) -- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) -- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) -- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) -- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) -- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) -- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) -- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) -- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) -- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) -- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) -- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) -- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) -- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) -- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) -- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) -- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) -- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) - [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) - [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) - [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) - [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) +- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/index.html.md) +- [sendNotificationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) - [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) +- [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) +- [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) +- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) +- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) +- [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) +- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) +- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) +- [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) +- [createOrderExchangeItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangeItemsFromActionsStep/index.html.md) +- [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) +- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) +- [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) +- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) +- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) +- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) +- [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) +- [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) +- [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/index.html.md) +- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) +- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) +- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) +- [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) +- [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) +- [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) +- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) +- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) +- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) +- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) +- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) +- [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) +- [createPriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListPricesStep/index.html.md) +- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) +- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) +- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) +- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) +- [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) +- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) +- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) +- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) +- [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) +- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) +- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) +- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) +- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) +- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) +- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) +- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) +- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) +- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) +- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) +- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) +- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) +- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) +- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) +- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) +- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) +- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) +- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) +- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) +- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) +- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) +- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) +- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) +- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) +- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) +- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) +- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) +- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) +- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) +- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) +- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) +- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) +- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/index.html.md) +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) +- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) - [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) - [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) - [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) - [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) - [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) -- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) -- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) - [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) -- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) -- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) - [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) -- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) -- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) -- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) -- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) -- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) -- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) +- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) - [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) -- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) -- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) -- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) -- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) -- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) -- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) -- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) -- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) -- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) -- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) -- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) -- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) -- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) -- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) -- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) - [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) - [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) - [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) +- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) +- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) +- [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) +- [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) +- [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) +- [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) +- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) +- [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) +- [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) +- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) +- [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) +- [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) +- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) +- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) +- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) +- [canDeleteSalesChannelsOrThrowStep](https://docs.medusajs.com/references/medusa-workflows/steps/canDeleteSalesChannelsOrThrowStep/index.html.md) +- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/index.html.md) +- [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/index.html.md) +- [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) +- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) +- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) + + +# Events Reference + +This documentation page includes the list of all events emitted by [Medusa's workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md). + +## Cart Events + +### Summary + +|Event|Description| +|---|---| +|cart.created|Emitted when a cart is created.| +|cart.updated|Emitted when a cart's details are updated.| +|cart.customer\_updated|Emitted when the customer in the cart is updated.| +|cart.region\_updated|Emitted when the cart's region is updated. This +event is emitted alongside the CartWorkflowEvents.UPDATED event.| + +### `cart.created` + +Emitted when a cart is created. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) + +*** + +### `cart.updated` + +Emitted when a cart's details are updated. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) +- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) + +*** + +### `cart.customer_updated` + +Emitted when the customer in the cart is updated. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) + +*** + +### `cart.region_updated` + +Emitted when the cart's region is updated. This +event is emitted alongside the CartWorkflowEvents.UPDATED event. + +#### Payload + +```ts +{ + id, // The ID of the cart +} +``` + +#### Workflows Emitting this Event + +- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) + +*** + +## Customer Events + +### Summary + +|Event|Description| +|---|---| +|customer.created|Emitted when a customer is created.| +|customer.updated|Emitted when a customer is updated.| +|customer.deleted|Emitted when a customer is deleted.| + +### `customer.created` + +Emitted when a customer is created. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) + +*** + +### `customer.updated` + +Emitted when a customer is updated. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) + +*** + +### `customer.deleted` + +Emitted when a customer is deleted. + +#### Payload + +```ts +[{ + id, // The ID of the customer +}] +``` + +#### Workflows Emitting this Event + +- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) +- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) + +*** + +## Order Events + +### Summary + +|Event|Description| +|---|---| +|order.updated|Emitted when the details of an order or draft order is updated. This +doesn't include updates made by an edit.| +|order.placed|Emitted when an order is placed, or when a draft order is converted to an +order.| +|order.canceled|Emitted when an order is canceld.| +|order.completed|Emitted when orders are completed.| +|order.archived|Emitted when an order is archived.| +|order.fulfillment\_created|Emitted when a fulfillment is created for an order.| +|order.fulfillment\_canceled|Emitted when an order's fulfillment is canceled.| +|order.return\_requested|Emitted when a return request is confirmed.| +|order.return\_received|Emitted when a return is marked as received.| +|order.claim\_created|Emitted when a claim is created for an order.| +|order.exchange\_created|Emitted when an exchange is created for an order.| +|order.transfer\_requested|Emitted when an order is requested to be transferred to +another customer.| + +### `order.updated` + +Emitted when the details of an order or draft order is updated. This +doesn't include updates made by an edit. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) +- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) + +*** + +### `order.placed` + +Emitted when an order is placed, or when a draft order is converted to an +order. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) + +*** + +### `order.canceled` + +Emitted when an order is canceld. + +#### Payload + +```ts +{ + id, // The ID of the order +} +``` + +#### Workflows Emitting this Event + +- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) + +*** + +### `order.completed` + +Emitted when orders are completed. + +#### Payload + +```ts +[{ + id, // The ID of the order +}] +``` + +#### Workflows Emitting this Event + +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) + +*** + +### `order.archived` + +Emitted when an order is archived. + +#### Payload + +```ts +[{ + id, // The ID of the order +}] +``` + +#### Workflows Emitting this Event + +- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) + +*** + +### `order.fulfillment_created` + +Emitted when a fulfillment is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + fulfillment_id, // The ID of the fulfillment + no_notification, // Whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +*** + +### `order.fulfillment_canceled` + +Emitted when an order's fulfillment is canceled. + +#### Payload + +```ts +{ + order_id, // The ID of the order + fulfillment_id, // The ID of the fulfillment + no_notification, // Whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) + +*** + +### `order.return_requested` + +Emitted when a return request is confirmed. + +#### Payload + +```ts +{ + order_id, // The ID of the order + return_id, // The ID of the return +} +``` + +#### Workflows Emitting this Event + +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) + +*** + +### `order.return_received` + +Emitted when a return is marked as received. + +#### Payload + +```ts +{ + order_id, // The ID of the order + return_id, // The ID of the return +} +``` + +#### Workflows Emitting this Event + +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +*** + +### `order.claim_created` + +Emitted when a claim is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + claim_id, // The ID of the claim +} +``` + +#### Workflows Emitting this Event + +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) + +*** + +### `order.exchange_created` + +Emitted when an exchange is created for an order. + +#### Payload + +```ts +{ + order_id, // The ID of the order + exchange_id, // The ID of the exchange +} +``` + +#### Workflows Emitting this Event + +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) + +*** + +### `order.transfer_requested` + +Emitted when an order is requested to be transferred to +another customer. + +#### Payload + +```ts +{ + id, // The ID of the order + order_change_id, // The ID of the order change created for the transfer +} +``` + +#### Workflows Emitting this Event + +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) + +*** + +## Order Edit Events + +### Summary + +|Event|Description| +|---|---| +|order-edit.requested|Emitted when an order edit is requested.| +|order-edit.confirmed|Emitted when an order edit request is confirmed.| +|order-edit.canceled|Emitted when an order edit request is canceled.| + +### `order-edit.requested` + +Emitted when an order edit is requested. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // The actions to edit the order +} +``` + +#### Workflows Emitting this Event + +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) + +*** + +### `order-edit.confirmed` + +Emitted when an order edit request is confirmed. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // The actions to edit the order +} +``` + +#### Workflows Emitting this Event + +- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) + +*** + +### `order-edit.canceled` + +Emitted when an order edit request is canceled. + +#### Payload + +```ts +{ + order_id, // The ID of the order + actions, // The actions to edit the order +} +``` + +#### Workflows Emitting this Event + +- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) + +*** + +## User Events + +### Summary + +|Event|Description| +|---|---| +|user.created|Emitted when users are created.| +|user.updated|Emitted when users are updated.| +|user.deleted|Emitted when users are deleted.| + +### `user.created` + +Emitted when users are created. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) +- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) + +*** + +### `user.updated` + +Emitted when users are updated. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +- [updateUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateUsersWorkflow/index.html.md) + +*** + +### `user.deleted` + +Emitted when users are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the user +}] +``` + +#### Workflows Emitting this Event + +- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [removeUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeUserAccountWorkflow/index.html.md) + +*** + +## Invite Events + +### Summary + +|Event|Description| +|---|---| +|invite.accepted|Emitted when an invite is accepted.| +|invite.created|Emitted when invites are created. You can listen to this event +to send an email to the invited users, for example.| +|invite.deleted|Emitted when invites are deleted.| +|invite.resent|Emitted when invites should be resent because their token was +refreshed. You can listen to this event to send an email to the invited users, +for example.| + +### `invite.accepted` + +Emitted when an invite is accepted. + +#### Payload + +```ts +{ + id, // The ID of the invite +} +``` + +#### Workflows Emitting this Event + +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) + +*** + +### `invite.created` + +Emitted when invites are created. You can listen to this event +to send an email to the invited users, for example. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) + +*** + +### `invite.deleted` + +Emitted when invites are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) + +*** + +### `invite.resent` + +Emitted when invites should be resent because their token was +refreshed. You can listen to this event to send an email to the invited users, +for example. + +#### Payload + +```ts +[{ + id, // The ID of the invite +}] +``` + +#### Workflows Emitting this Event + +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) + +*** + +## Auth Events + +### Summary + +|Event|Description| +|---|---| +|auth.password\_reset|Emitted when a reset password token is generated. You can listen to this event +to send a reset password email to the user or customer, for example.| + +### `auth.password_reset` + +Emitted when a reset password token is generated. You can listen to this event +to send a reset password email to the user or customer, for example. + +#### Payload + +```ts +{ + entity_id, // The identifier of the user or customer. For example, an email address. + actor_type, // The type of actor. For example, "customer", "user", or custom. + token, // The generated token. +} +``` + +#### Workflows Emitting this Event + +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) + +*** + +## Sales Channel Events + +### Summary + +|Event|Description| +|---|---| +|sales-channel.created|Emitted when sales channels are created.| +|sales-channel.updated|Emitted when sales channels are updated.| +|sales-channel.deleted|Emitted when sales channels are deleted.| + +### `sales-channel.created` + +Emitted when sales channels are created. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) + +*** + +### `sales-channel.updated` + +Emitted when sales channels are updated. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) + +*** + +### `sales-channel.deleted` + +Emitted when sales channels are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the sales channel +}] +``` + +#### Workflows Emitting this Event + +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) + +*** + +## Product Category Events + +### Summary + +|Event|Description| +|---|---| +|product-category.created|Emitted when product categories are created.| +|product-category.updated|Emitted when product categories are updated.| +|product-category.deleted|Emitted when product categories are deleted.| + +### `product-category.created` + +Emitted when product categories are created. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) + +*** + +### `product-category.updated` + +Emitted when product categories are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) + +*** + +### `product-category.deleted` + +Emitted when product categories are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product category +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) + +*** + +## Product Collection Events + +### Summary + +|Event|Description| +|---|---| +|product-collection.created|Emitted when product collections are created.| +|product-collection.updated|Emitted when product collections are updated.| +|product-collection.deleted|Emitted when product collections are deleted.| + +### `product-collection.created` + +Emitted when product collections are created. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +- [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) + +*** + +### `product-collection.updated` + +Emitted when product collections are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +- [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) + +*** + +### `product-collection.deleted` + +Emitted when product collections are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product collection +}] +``` + +#### Workflows Emitting this Event + +- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) + +*** + +## Product Variant Events + +### Summary + +|Event|Description| +|---|---| +|product-variant.updated|Emitted when product variants are updated.| +|product-variant.created|Emitted when product variants are created.| +|product-variant.deleted|Emitted when product variants are deleted.| + +### `product-variant.updated` + +Emitted when product variants are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +- [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) + +*** + +### `product-variant.created` + +Emitted when product variants are created. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +- [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### `product-variant.deleted` + +Emitted when product variants are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product variant +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) + +*** + +## Product Events + +### Summary + +|Event|Description| +|---|---| +|product.updated|Emitted when products are updated.| +|product.created|Emitted when products are created.| +|product.deleted|Emitted when products are deleted.| + +### `product.updated` + +Emitted when products are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### `product.created` + +Emitted when products are created. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +### `product.deleted` + +Emitted when products are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) + +*** + +## Product Type Events + +### Summary + +|Event|Description| +|---|---| +|product-type.updated|Emitted when product types are updated.| +|product-type.created|Emitted when product types are created.| +|product-type.deleted|Emitted when product types are deleted.| + +### `product-type.updated` + +Emitted when product types are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +- [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) + +*** + +### `product-type.created` + +Emitted when product types are created. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +- [createProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTypesWorkflow/index.html.md) + +*** + +### `product-type.deleted` + +Emitted when product types are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product type +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) + +*** + +## Product Tag Events + +### Summary + +|Event|Description| +|---|---| +|product-tag.updated|Emitted when product tags are updated.| +|product-tag.created|Emitted when product tags are created.| +|product-tag.deleted|Emitted when product tags are deleted.| + +### `product-tag.updated` + +Emitted when product tags are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) + +*** + +### `product-tag.created` + +Emitted when product tags are created. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) + +*** + +### `product-tag.deleted` + +Emitted when product tags are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product tag +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) + +*** + +## Product Option Events + +### Summary + +|Event|Description| +|---|---| +|product-option.updated|Emitted when product options are updated.| +|product-option.created|Emitted when product options are created.| +|product-option.deleted|Emitted when product options are deleted.| + +### `product-option.updated` + +Emitted when product options are updated. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) + +*** + +### `product-option.created` + +Emitted when product options are created. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +- [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) + +*** + +### `product-option.deleted` + +Emitted when product options are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the product option +}] +``` + +#### Workflows Emitting this Event + +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) + +*** + +## Region Events + +### Summary + +|Event|Description| +|---|---| +|region.updated|Emitted when regions are updated.| +|region.created|Emitted when regions are created.| +|region.deleted|Emitted when regions are deleted.| + +### `region.updated` + +Emitted when regions are updated. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) + +*** + +### `region.created` + +Emitted when regions are created. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) + +*** + +### `region.deleted` + +Emitted when regions are deleted. + +#### Payload + +```ts +[{ + id, // The ID of the region +}] +``` + +#### Workflows Emitting this Event + +- [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) + +*** + +## Fulfillment Events + +### Summary + +|Event|Description| +|---|---| +|shipment.created|Emitted when a shipment is created for an order.| +|delivery.created|Emitted when a fulfillment is marked as delivered.| + +### `shipment.created` + +Emitted when a shipment is created for an order. + +#### Payload + +```ts +{ + id, // the ID of the shipment + no_notification, // whether to notify the customer +} +``` + +#### Workflows Emitting this Event + +- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) + +*** + +### `delivery.created` + +Emitted when a fulfillment is marked as delivered. + +#### Payload + +```ts +{ + id, // the ID of the fulfillment +} +``` + +#### Workflows Emitting this Event + +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) # Medusa CLI Reference @@ -31065,68 +32364,6 @@ npx medusa --help *** -# build Command - Medusa CLI Reference - -Create a standalone build of the Medusa application. - -This creates a build that: - -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. - -The build is outputted to a new `.medusa/server` directory. - -```bash -npx medusa build -``` - -Refer to [this section](#run-built-medusa-application) for next steps. - -## Options - -|Option|Description| -|---|---|---| -|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | - -*** - -## Run Built Medusa Application - -After running the `build` command, use the following step to run the built Medusa application: - -- Change to the `.medusa/server` directory and install the dependencies: - -```bash npm2yarn -cd .medusa/server && npm install -``` - -- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. - -```bash npm2yarn -cp .env .medusa/server/.env.production -``` - -- In the system environment variables, set `NODE_ENV` to `production`: - -```bash -NODE_ENV=production -``` - -- Use the `start` command to run the application: - -```bash npm2yarn -cd .medusa/server && npm run start -``` - -*** - -## Build Medusa Admin - -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. - -If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -31308,39 +32545,6 @@ medusa new [ []] |\`--db-host \\`|The database host to use for database setup.| -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| -|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| - - -# telemetry Command - Medusa CLI Reference - -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. - -```bash -npx medusa telemetry -``` - -#### Options - -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| - - # plugin Commands - Medusa CLI Reference Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. @@ -31402,6 +32606,101 @@ npx medusa plugin:build ``` +# build Command - Medusa CLI Reference + +Create a standalone build of the Medusa application. + +This creates a build that: + +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. + +The build is outputted to a new `.medusa/server` directory. + +```bash +npx medusa build +``` + +Refer to [this section](#run-built-medusa-application) for next steps. + +## Options + +|Option|Description| +|---|---|---| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | + +*** + +## Run Built Medusa Application + +After running the `build` command, use the following step to run the built Medusa application: + +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install +``` + +- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. + +```bash npm2yarn +cp .env .medusa/server/.env.production +``` + +- In the system environment variables, set `NODE_ENV` to `production`: + +```bash +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start +``` + +*** + +## Build Medusa Admin + +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. + +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. + + +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| +|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -31506,6 +32805,22 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. +# develop Command - Medusa CLI Reference + +Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. + +```bash +npx medusa develop +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| + + # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -31626,6 +32941,35 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + # plugin Commands - Medusa CLI Reference Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. @@ -31687,38 +33031,6 @@ npx medusa plugin:build ``` -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# exec Command - Medusa CLI Reference - -Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). - -```bash -npx medusa exec [file] [args...] -``` - -## Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| -|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| - - # start Command - Medusa CLI Reference Start the Medusa application in production. @@ -31736,33 +33048,20 @@ npx medusa start |\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| -# new Command - Medusa CLI Reference +# exec Command - Medusa CLI Reference -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. +Run a custom CLI script. Learn more about it in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/custom-cli-scripts/index.html.md). ```bash -medusa new [ []] +npx medusa exec [file] [args...] ``` ## Arguments -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| - -## Options - -|Option|Description| -|---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| +|Argument|Description|Required| +|---|---|---|---|---| +|\`file\`|The path to the TypeScript or JavaScript file holding the function to execute.|Yes| +|\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| # user Command - Medusa CLI Reference @@ -47581,6 +48880,1820 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# Integrate Medusa with Sanity (CMS) + +In this guide, you'll learn how to integrate Medusa with Sanity. + +When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features. + +Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing. + +This guide will teach you how to: + +- Install and set up Medusa. +- Install and set up Sanity with Medusa's Next.js Starter storefront. +- Sync product data from Medusa to Sanity when a product is created or updated. +- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs). + +[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js storefront, choose `Y` for yes. + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js storefront in a directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. + +Afterwards, you can login with the new user and explore the dashboard. The Next.js storefront is also running at `http://localhost:8000`. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Install Sanity Client SDK + +In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity. + +In your terminal, move to the Medusa application's directory and run the following command: + +```bash npm2yarn +cd project-name # replace with directory name +npm install @sanity/client +``` + +*** + +## Step 3: Create a Sanity Project + +When the Medusa application connects to Sanity, it must connect to a project in Sanity. + +So, before building the integration in Medusa, create a project in Sanity using their website: + +1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login) +2. On your account's dashboard, click the "Create new project" button. + +![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png) + +3. Enter a project name and click "Create Project" + +![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png) + +You'll go back to the project's setting page in a later step. + +*** + +## Step 4: Create Sanity Module + +To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it. + +Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +### Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/sanity`. + +### Create Service + +You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. + +Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods. + +The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity. + +Start by creating the file `src/modules/sanity/service.ts` with the following content: + +```ts title="src/modules/sanity/service.ts" +import { + Logger, +} from "@medusajs/framework/types" +import { + SanityClient, +} from "@sanity/client" + +class SanityModuleService { + private client: SanityClient + private studioUrl?: string + private logger: Logger + + // TODO +} + +export default SanityModuleService +``` + +You create the `SanityModuleService` class that for now only has three properties: + +- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity. +- `studioUrl` property which will hold the URL to access the Sanity studio. +- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. + +In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID. + +So, add after the import at the top of the file the following types: + +```ts title="src/modules/sanity/service.ts" +// other imports... + +const SyncDocumentTypes = { + PRODUCT: "product", +} as const + +type SyncDocumentTypes = + (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes]; + +type ModuleOptions = { + api_token: string; + project_id: string; + api_version: string; + dataset: "production" | "development"; + type_map?: Record; + studio_url?: string; +} +``` + +The `ModuleOptions` type defines the type of options that the module expects: + +- `api_token`: API token to connect to Sanity. +- `project_id`: The ID of the Sanity project. +- `api_version`: The Sanity API version. +- `dataset`: The dataset to use, which is either `production` or `development`. +- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too. +- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard. + +You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`: + +```ts title="src/modules/sanity/service.ts" +import { + // other imports... + createClient, +} from "@sanity/client" + +// types... + +type InjectedDependencies = { + logger: Logger +}; + +class SanityModuleService { + // properties... + constructor({ + logger, + }: InjectedDependencies, options: ModuleOptions) { + this.client = createClient({ + projectId: options.project_id, + apiVersion: options.api_version, + dataset: options.dataset, + token: options.api_token, + }) + this.logger = logger + + this.logger.info("Connected to Sanity") + + this.studioUrl = options.studio_url + + // TODO initialize more properties + } +} +``` + +The service's constructor accepts two parameters: + +1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). +2. The options passed to the module. + +In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives. + +You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful. + +#### Transform Product Data + +When you create or update products in Sanity, you must prepare the product object based on what Sanity expects. + +So, you'll add methods to the service that transform a Medusa product to a Sanity document object. + +Start by adding the following types and class properties to `src/modules/sanity/service.ts`: + +```ts title="src/modules/sanity/service.ts" +type SyncDocumentInputs = T extends "product" + ? ProductDTO + : never + +type TransformationMap = Record< + SyncDocumentTypes, + (data: SyncDocumentInputs) => any +>; + +class SanityModuleService { + // other properties... + private typeMap: Record + private createTransformationMap: TransformationMap + private updateTransformationMap: TransformationMap + + // ... +} +``` + +First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data. + +Then, in the service, you define three new properties: + +- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity. +- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created. +- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation. + +Next, add the following two methods to transform a product: + +```ts title="src/modules/sanity/service.ts" +// other imports... +import { + ProductDTO, +} from "@medusajs/framework/types" + +class SanityModuleService { + // ... + private transformProductForCreate = (product: ProductDTO) => { + return { + _type: this.typeMap[SyncDocumentTypes.PRODUCT], + _id: product.id, + title: product.title, + specs: [ + { + _key: product.id, + _type: "spec", + title: product.title, + lang: "en", + }, + ], + } + } + + private transformProductForUpdate = (product: ProductDTO) => { + return { + set: { + title: product.title, + }, + } + } +} +``` + +The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document. + +The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods. + +Finally, initialize the new properties you added in the `SanityModuleService`'s constructor: + +```ts title="src/modules/sanity/service.ts" +class SanityModuleService { + // ... + constructor({ + logger, + }: InjectedDependencies, options: ModuleOptions) { + // ... + this.typeMap = Object.assign( + {}, + { + [SyncDocumentTypes.PRODUCT]: "product", + }, + options.type_map || {} + ) + + this.createTransformationMap = { + [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate, + } + + this.updateTransformationMap = { + [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate, + } + } + // ... +} +``` + +You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update. + +You can modify these properties to add support for other schema types, such as product categories or collections. + +#### Methods to Manage Documents + +In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity. + +Add the following methods to the `SanityModuleService` class: + +```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights} +// other imports... +import { + // ... + FirstDocumentMutationOptions, +} from "@sanity/client" + +class SanityModuleService { + // ... + async upsertSyncDocument( + type: T, + data: SyncDocumentInputs + ) { + const existing = await this.client.getDocument(data.id) + if (existing) { + return await this.updateSyncDocument(type, data) + } + + return await this.createSyncDocument(type, data) + } + + async createSyncDocument( + type: T, + data: SyncDocumentInputs, + options?: FirstDocumentMutationOptions + ) { + const doc = this.createTransformationMap[type](data) + return await this.client.create(doc, options) + } + + async updateSyncDocument( + type: T, + data: SyncDocumentInputs + ) { + const operations = this.updateTransformationMap[type](data) + return await this.client.patch(data.id, operations).commit() + } +} +``` + +You add three methods: + +- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa. +- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). +- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). + +You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`: + +```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights} +class SanityModuleService { + // ... + async retrieve(id: string) { + return this.client.getDocument(id) + } + + async delete(id: string) { + return this.client.delete(id) + } + + async update(id: string, data: any) { + return await this.client.patch(id, { + set: data, + }).commit() + } + + async list( + filter: { + id: string | string[] + } + ) { + const data = await this.client.getDocuments( + Array.isArray(filter.id) ? filter.id : [filter.id] + ) + + return data.map((doc) => ({ + id: doc?._id, + ...doc, + })) + } +} +``` + +You add other three methods: + +- `retrieve` to retrieve a document by its ID. +- `delete` to delete a document by its ID. +- `update` to update a document by its ID with new data. +- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents. + +### Export Module Definition + +The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity. + +Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service. + +Create the file `src/modules/sanity/index.ts` with the following content: + +```ts title="src/modules/sanity/index.ts" +import { Module } from "@medusajs/framework/utils" +import SanityModuleService from "./service" + +export const SANITY_MODULE = "sanity" + +export default Module(SANITY_MODULE, { + service: SanityModuleService, +}) +``` + +In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container. + +You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service. + +### Add Module to Configurations + +Finally, to register a module in Medusa, you must add it to Medusa's configurations. + +Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application. + +Add the `modules` property to the exported configurations in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/sanity", + options: { + api_token: process.env.SANITY_API_TOKEN, + project_id: process.env.SANITY_PROJECT_ID, + api_version: new Date().toISOString().split("T")[0], + dataset: "production", + studio_url: process.env.SANITY_STUDIO_URL || + "http://localhost:3000/studio", + type_map: { + product: "product", + }, + }, + }, + ], +}) +``` + +In the `modules` array, you pass a module object having the following properties: + +- `resolve`: The path to the module to register in the application. It can also be the name of an NPM package. +- `options`: An object of options to pass to the module. These are the options you expect and use in the module's service. + +Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to `.env`: + +```shell +SANITY_API_TOKEN= +SANITY_PROJECT_ID= +SANITY_STUDIO_URL=http://localhost:8000/studio +``` + +Where: + +- `SANITY_API_TOKEN`: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard: + - Go to the API tab. + +![The API tab is at the top of the project dashboard next to Settings.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) + +- Scroll down to Tokens and click on the "Add API Token" button. + +![The Add API token button is at the top right of the Tokens section.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091810/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.52_AM_ccgsum.png) + +- Enter a name for the API token, choose "Editor" for the permissions, then click Save. + +![In the Token form, enter the name and choose "Editor" for permisions.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091811/Medusa%20Resources/Screenshot_2024-11-20_at_10.36.25_AM_nqxa5y.png) + +- `SANITY_PROJECT_ID`: The ID of the project, which you can find at the top section of your Sanity project's dashboard. + +![The project ID is in the top information of the project.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091988/Medusa%20Resources/Screenshot_2024-11-20_at_10.39.24_AM_cscir8.png) + +- `SANITY_STUDIO_URL`: The URL to access the studio. You'll set the studio up in a later section, but for now set it to `http://localhost:8000/studio`. + +### Test the Module + +To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console. + +To start the Medusa application, run the following command in the application's directory: + +```bash npm2yarn +npm run dev +``` + +If you see the following message among the logs: + +```bash +info: Connected to Sanity +``` + +That means your Sanity credentials were correct, and Medusa was able to connect to Sanity. + +In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data. + +*** + +## Step 5: Link Product and Sanity Modules + +Since a product has a document in Sanity, you want to build an association between the [Product](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document. + +However, modules are [isolated](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](https://docs.medusajs.com/docs/learn/fundamentals/module-links/index.html.md). + +A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems. + +In this section, you'll define a link between the Product and Sanity modules. + +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. So, create the file `src/links/product-sanity.ts` with the following content: + +```ts title="src/links/product-sanity.ts" +import { defineLink } from "@medusajs/framework/utils" +import ProductModule from "@medusajs/medusa/product" +import { SANITY_MODULE } from "../modules/sanity" + +defineLink( + { + ...ProductModule.linkable.product.id, + field: "id", + }, + { + linkable: { + serviceName: SANITY_MODULE, + alias: "sanity_product", + primaryKey: "id", + }, + }, + { + readOnly: true, + } +) +``` + +You define a link using `defineLink` from the Modules SDK. It accepts three parameters: + +1. The first data model part of the link. In this case, it's the Product Module's `product` data model. A module has a special `linkable` property that contain link configurations for its data models. +2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a `linkable` object that has the following properties: + - `serviceName`: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name). + - `alias`: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section. + - `primaryKey`: The name of the data model's primary key field. +3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the `readOnly` configuration, which will tell Medusa not to create a table in the database for this link. + +In the next steps, you'll see how this link allows you to retrieve documents when retrieving products. + +*** + +## Step 6: Sync Data to Sanity + +After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need a workflow. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. You'll see how all of this works in the upcoming sections. + +Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route. + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) + +In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually. + +### Create Step + +The syncing workflow will have a single step that syncs products provided as an input to Sanity. + +So, to implement that step, create the file `src/workflows/sanity-sync-products/steps/sync.ts` with the following content: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" highlights={syncStepHighlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { ProductDTO } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + promiseAll, +} from "@medusajs/framework/utils" +import SanityModuleService from "../../../modules/sanity/service" +import { SANITY_MODULE } from "../../../modules/sanity" + +export type SyncStepInput = { + product_ids?: string[]; +} + +export const syncStep = createStep( + { name: "sync-step", async: true }, + async (input: SyncStepInput, { container }) => { + const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const total = 0 + const upsertMap: { + before: any + after: any + }[] = [] + + const batchSize = 200 + const hasMore = true + const offset = 0 + const filters = input.product_ids ? { + id: input.product_ids, + } : {} + + while (hasMore) { + const { + data: products, + metadata: { count } = {}, + } = await query.graph({ + entity: "product", + fields: [ + "id", + "title", + // @ts-ignore + "sanity_product.*", + ], + filters, + pagination: { + skip: offset, + take: batchSize, + order: { + id: "ASC", + }, + }, + }) + + // TODO sync products + } + } +) +``` + +You define the `syncStep` using the `createStep` function, which accepts two parameters: + +- An object of step configurations. The object must have the `name` property, which is this step's unique name. Enabling the `async` property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing. +- The step's function definition as a second parameter. + +The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources. + +In the step, you resolve from the Medusa container Sanity Module's service and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that allows you to retrieve data across modules and links. + +You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve. + +Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retrieve the Sanity document of each product using Sanity Module's `list` method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this: + +```json title="Example Product Object" +{ + "id": "prod_123", + "title": "Shirt", + "sanity_product": { + "id": "prod_123", + "_type": "product", + // other Sanity fields... + } +} +``` + +Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +while (hasMore) { + // ... + try { + await promiseAll( + products.map(async (prod) => { + const after = await sanityModule.upsertSyncDocument( + "product", + prod as ProductDTO + ) + + upsertMap.push({ + // @ts-ignore + before: prod.sanity_product, + after, + }) + + return after + }) + ) + } catch (e) { + return StepResponse.permanentFailure( + `An error occurred while syncing documents: ${e}`, + upsertMap + ) + } + + offset += batchSize + hasMore = offset < count + total += products.length +} +``` + +In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. + +For each product, you upsert it into Sanity, then push its document before and after the update to the `upsertMap`. You'll learn more about its use later. + +You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism. + +Finally, after the `while` loop and at the end of the step, add the following return statement: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +return new StepResponse({ total }, upsertMap) +``` + +If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step. + +#### Add Compensation Function + +`StepResponse` accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully. + +Learn more about compensation functions in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/compensation-function/index.html.md). + +The `syncStep` creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs. + +To define the compensation function, pass a third-parameter to the `createStep` function: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +export const syncStep = createStep( + { name: "sync-step", async: true }, + async (input: SyncStepInput, { container }) => { + // ... + }, + async (upsertMap, { container }) => { + if (!upsertMap) { + return + } + + const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) + + await promiseAll( + upsertMap.map(({ before, after }) => { + if (!before) { + // delete the document + return sanityModule.delete(after._id) + } + + const { _id: id, ...oldData } = before + + return sanityModule.update( + id, + oldData + ) + }) + ) + } +) +``` + +The compensation function accepts the data passed in the step's `StepResponse` second parameter (in this case, `upsertMap`), and an object of options similar to that of the step. + +In the compensation function, you resolve the Sanity Module's service, then loop over the `upsertMap` to delete created documents, or revert existing ones. + +### Create Workflow + +You'll now create the workflow that uses the `syncStep`. This is the workflow that you'll later execute to sync data automatically or manually. + +Workflows are created in a file under the `src/workflows` directory. So, create the file `src/workflows/sanity-sync-products/index.ts` with the following content: + +```ts title="src/workflows/sanity-sync-products/index.ts" +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { syncStep } from "./steps/sync" + +export type SanitySyncProductsWorkflowInput = { + product_ids?: string[]; +}; + +export const sanitySyncProductsWorkflow = createWorkflow( + { name: "sanity-sync-products", retentionTime: 10000 }, + function (input: SanitySyncProductsWorkflowInput) { + const result = syncStep(input) + + return new WorkflowResponse(result) + } +) +``` + +You create a workflow using the `createWorkflow` from the Workflows SDK. It accepts an object of options as a first parameter, where the `name` property is required and indicates the workflow's unique name. + +The `retentionTime` property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing. + +`createWorkflow` accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the `syncStep` to sync the specified products in the input, then return its result. Workflows must return an instance of `WorkflowResponse`. + +A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). + +You'll execute and test this workflow in the next steps. + +*** + +## Step 7: Handle Product Changes in Medusa + +You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated. + +Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber. + +A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application. + +Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created. + +Learn more about events and subscribers in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). You can also find the list of emitted events in [this reference](https://docs.medusajs.com/references/events/index.html.md). + +So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the `product.created` and `product.updated` events. + +Subscribers are created under the `src/subscribers` directory. So, create the file `src/subscribers/sanity-product-sync.ts` with the following content: + +```ts title="src/subscribers/sanity-product-sync.ts" highlights={subscriberHighlights} +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/medusa" +import { + sanitySyncProductsWorkflow, +} from "../workflows/sanity-sync-products" + +export default async function upsertSanityProduct({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sanitySyncProductsWorkflow(container).run({ + input: { + product_ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: ["product.created", "product.updated"], +} +``` + +The subscriber function `upsertSanityProduct` accepts an object as a parameter that has the following properties: + +- `event`: An object of the event's details. Its `data` property holds the data payload emitted with the event, which in this case is the ID of the product created or updated. +- `container`: An instance of the Medusa container to resolve resources. + +In the subscriber, you execute the `sanitySyncProductsWorkflow` by invoking it, passing it the container, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run`'s object parameter. + +The subscriber file must also export a configuration object. It has an `event` property, which is the names of the events that the subscriber is listening to. + +### Test it Out + +To test it out, run the Medusa application, then open the Medusa Admin in your browser at `http://localhost:9000/app`. Try creating or updating a product. You'll see the following message in the console: + +```bash +info: Processing product.created which has 1 subscribers +``` + +This means that the `product.created` event was emitted and your subscriber was executed. + +In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio. + +*** + +## Step 8: Setup Sanity with Next.js Starter Storefront + +In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page. + +Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory). + +Then, run the following command: + +```bash badgeLabel="Storefront" badgeColor="blue" +npx sanity@latest init +``` + +You'll then be asked a few questions: + +- For the project, select the Sanity project you created earlier in this guide. +- For dataset, use `production` unless you changed it in the Sanity project. +- Select yes for adding the Sanity configuration files to the Next.js folder. +- Select yes for TypeScript. +- Select yes for Sanity studio, and choose the `/studio` route. +- Select clean project template. +- Select yes for adding the project ID and dataset to `.env.local`. + +Afterwards, the command will install the necessary dependencies for Sanity. + +### Error during installation + +If you run into an error during the installation of peer dependencies, try running the following command to install them: + +```bash +yarn add next-sanity@9.8.15 @sanity/client@^6.22.4 @sanity/icons@^3.4.0 @sanity/types@^3.62.0 @sanity/ui@^2.8.10 next@^15.0.0 react@^19.0.0 react-dom@^19.0.0 sanity@^3.62.0 styled-components@^6.1 +``` + +### Update Middleware + +The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, `/us`). + +Since the Sanity studio runs at `/studio`, the middleware should ignore requests to this path. + +Open the file `src/middleware.ts` and find the following `if` condition: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +if ( + urlHasCountryCode && + (!isOnboarding || onboardingCookie) && + (!cartId || cartIdCookie) +) { + return NextResponse.next() +} +``` + +Replace it with the following condition: + +```ts title="src/middleware.ts" badgeLabel="Storefront" badgeColor="blue" +if ( + request.nextUrl.pathname.startsWith("/studio") || + urlHasCountryCode && + (!isOnboarding || onboardingCookie) && + (!cartId || cartIdCookie) +) { + return NextResponse.next() +} +``` + +If the path starts with `/studio`, the middleware will stop executing and the page will open. + +### Set CORS Settings + +Every Sanity project has a configured set of CORS origins allowed, with the default being `http://localhost:3333`. + +The Next.js Starter runs on the `8000` port, so you must add it to the allowed CORS origins. + +In your Sanity project's dashboard: + +1. Click on the API tab. + +![Find the API tab at the top of the dashboard.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096643/Medusa%20Resources/Screenshot_2024-11-20_at_10.35.29_AM_ltj7cd.png) + +2. Scroll down to CORS origins and click the "Add CORS origin" button. + +![Find the CORS origins section and click the Add CORS origin button at its top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732096997/Medusa%20Resources/Screenshot_2024-11-20_at_12.02.50_PM_ahsthb.png) + +3. Enter `http://localhost:8000` in the Origin field. +4. Enable the "Allow credentials" checkbox. + +![After filling out the Origin field, click on the Allow credentials checkbox to enable it.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732097074/Medusa%20Resources/Screenshot_2024-11-20_at_12.04.09_PM_nxdvwh.png) + +5. Click the Save button. + +### Open Sanity Studio + +To open the Sanity studio, start the Next.js Starter's development server: + +```bash npm2yarn +npm run dev +``` + +Then, open `http://localhost:8000/studio` in your browser. The Sanity studio will open, but right now it's empty. + +*** + +## Step 9: Add Product Schema Type in Sanity + +In this step, you'll define the `product` schema type in Sanity. You' can then view the documents of that schema in the studio and update their content. + +To create the schema type, create the file `src/sanity/schemaTypes/documents/product.ts` with the following content: + +```ts title="src/sanity/schemaTypes/documents/product.ts" badgeLabel="Storefront" badgeColor="blue" +import { ComposeIcon } from "@sanity/icons" +import { DocumentDefinition } from "sanity" + +const productSchema: DocumentDefinition = { + fields: [ + { + name: "title", + type: "string", + }, + { + group: "content", + name: "specs", + of: [ + { + fields: [ + { name: "lang", title: "Language", type: "string" }, + { name: "title", title: "Title", type: "string" }, + { + name: "content", + rows: 3, + title: "Content", + type: "text", + }, + ], + name: "spec", + type: "object", + }, + ], + type: "array", + }, + { + fields: [ + { name: "title", title: "Title", type: "string" }, + { + name: "products", + of: [{ to: [{ type: "product" }], type: "reference" }], + title: "Addons", + type: "array", + validation: (Rule) => Rule.max(3), + }, + ], + name: "addons", + type: "object", + }, + ], + name: "product", + preview: { + select: { + title: "title", + }, + }, + title: "Product Page", + type: "document", + groups: [{ + default: true, + // @ts-ignore + icon: ComposeIcon, + name: "content", + title: "Content", + }], +} + +export default productSchema +``` + +This creates a schema that has the following fields: + +- `title`: The title of a document, which is in this case the product's type. +- `specs`: An array of product specs. Each object in the array has the following fields: + - `lang`: This is useful if you want to have localized content. + - `title`: The product's title. + - `content`: Textual content, such as the product's description. +- `addons`: An object of products related to this product. + +When you sync the products from Medusa, you only sync the title. You manage the `specs` and `addons` fields within Sanity. + +Next, replace the content of `src/sanity/schemaTypes/index.ts` with the following: + +```ts title="src/sanity/schemaTypes/index.ts" badgeLabel="Storefront" badgeColor="blue" +import { SchemaPluginOptions } from "sanity" +import productSchema from "./documents/product" + +export const schema: SchemaPluginOptions = { + types: [productSchema], + templates: (templates) => templates.filter( + (template) => template.schemaType !== "product" + ), +} +``` + +You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa. + +### Test it Out + +To ensure that your schema is defined correctly and working, start the Next.js storefront's server, and open the Sanity studio again at `http://localhost:8000/studio`. + +You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa. + +If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio. + +If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page. + +*** + +## Step 10: Show Sanity Content in Next.js Starter Storefront + +Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity. + +A product's details are retrieved in the file `src/app/[countryCode]/(main)/products/[handle]/page.tsx`. So, replace the `ProductPage` function with the following: + +```tsx title="src/app/[countryCode]/(main)/products/[handle]/page.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={sanityContentHighlights} +// other imports... +import { client } from "../../../../../sanity/lib/client" + +// ... + +export default async function ProductPage(props: Props) { + const params = await props.params + const region = await getRegion(params.countryCode) + + if (!region) { + notFound() + } + + const pricedProduct = await listProducts({ + countryCode: params.countryCode, + queryParams: { handle: params.handle }, + }).then(({ response }) => response.products[0]) + + if (!pricedProduct) { + notFound() + } + + // alternatively, you can filter the content by the language + const sanity = (await client.getDocument(pricedProduct.id))?.specs[0] + + return ( + + ) +} +``` + +You import the Sanity client defined in `src/sanity/lib/client.ts` (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the `ProductTemplate` component. + +This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language. + +Next, you need to customize the `ProductTemplate` to accept the `sanity` prop. In the file `src/modules/products/templates/index.tsx` add the following to `ProductTemplateProps`: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductTemplateProps = { + // ... + sanity?: { + content: string + } +} +``` + +Then, add the `sanity` property to the expanded props of the component: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const ProductTemplate: React.FC = ({ + // ... + sanity, +}) => { + // ... +} +``` + +Finally, pass the `sanity` prop to the `ProductInfo` component in the return statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +Next, you need to update the `ProductInfo` component to accept and use the `sanity` prop. + +In `src/modules/products/templates/product-info/index.tsx`, update the `ProductInfoProps` to accept the `sanity` prop: + +```tsx title="src/modules/products/templates/product-info/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type ProductInfoProps = { + // ... + sanity?: { + content: string + } +} +``` + +Then, add the `sanity` property to the expanded props of the component: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const ProductInfo = ({ + // ... + sanity, +}: ProductInfoProps) => { + // ... +} +``` + +Next, find the following line in the return statement: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +{product.description} +``` + +And replace it with the following: + +```tsx title="src/modules/products/templates/index.tsx" badgeLabel="Storefront" badgeColor="blue" +{sanity?.content || product.description} +``` + +Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available. + +### Test it Out + +To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product. + +Then, open the Next.js Starter storefront at `http://localhost:8000` and go to "Store" from the menu, then select the product you edited in Sanity. + +In the product's page, you'll find under the product's name the content you put in Sanity. + +You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems. + +*** + +## Step 11: Customize Admin to Manually Sync Data + +There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration. + +The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](https://docs.medusajs.com/docs/learn/fundamentals/admin/widgets/index.html.md), into existing pages, or adding new pages, called [UI routes](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). In these customizations, you can send requests to the Medusa application to perform custom operations. + +In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually. + +![The widget in the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732093722/Medusa%20Resources/Screenshot_2024-11-20_at_11.08.23_AM_wzftfv.png) + +Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data. + +An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + +### Get Sanity Document API Route + +In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio. + +To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in `src/modules/sanity/service.ts`: + +```ts title="src/modules/sanity/service.ts" +class SanityModuleService { + // ... + async getStudioLink( + type: string, + id: string, + config: { explicit_type?: boolean } = {} + ) { + const resolvedType = config.explicit_type ? type : this.typeMap[type] + if (!this.studioUrl) { + throw new Error("No studio URL provided") + } + return `${this.studioUrl}/structure/${resolvedType};${id}` + } +} +``` + +The method uses the `studioUrl` property, which you set in the `constructor` using the `studio_url` module option, to get the studio link. + +Then, to create the API route, create the file `src/api/admin/sanity/documents/[id]/route.ts` with the following content: + +```ts title="src/api/admin/sanity/documents/[id]/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import SanityModuleService from "src/modules/sanity/service" +import { SANITY_MODULE } from "../../../../../modules/sanity" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { id } = req.params + + const sanityModule: SanityModuleService = req.scope.resolve( + SANITY_MODULE + ) + const sanityDocument = await sanityModule.retrieve(id) + + const url = sanityDocument ? + await sanityModule.getStudioLink( + sanityDocument._type, + sanityDocument._id, + { explicit_type: true } + ) + : "" + + res.json({ sanity_document: sanityDocument, studio_url: url }) +} +``` + +This defines a `GET` API route at `/admin/sanity/documents/:id`, where `:id` is a dynamic path parameter indicating the ID of a document to retrieve. + +In the `GET` route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document. + +You return in the JSON response an object having the `sanity_document` and `studio_url` properties. + +You'll test out this route in a later section. + +Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). + +### Trigger Sanity Sync API Route + +In this section, you'll add the API route that manually triggers syncing a product to Sanity. + +Since you already have the workflow to sync products, you only need to create an API route that executes it. + +Create the file `src/api/admin/sanity/documents/[id]/sync/route.ts` with the following content: + +```ts title="src/api/admin/sanity/documents/[id]/sync/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + sanitySyncProductsWorkflow, +} from "../../../../../../workflows/sanity-sync-products" + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { transaction } = await sanitySyncProductsWorkflow(req.scope) + .run({ + input: { product_ids: [req.params.id] }, + }) + + res.json({ transaction_id: transaction.transactionId }) +} +``` + +You add a `POST` API route at `/admin/sanity/documents/:id/sync`, where `:id` is a dynamic path parameter that indicates the ID of a product to sync to Sanity. + +In the `POST` API route handler, you execute the `sanitySyncProductsWorkflow`, passing it the ID of the product from the path parameter as an input. + +In the next section, you'll customize the admin dashboard and send requests to the API route from there. + +### Sanity Product Widget + +In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created. + +To send requests from admin customizations to the Medusa server, you need to use Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). You'll also use [Tanstack Query](https://tanstack.com/query/latest) to benefit from features like data caching and invalidation. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +To configure the JS SDK, create the file `src/admin/lib/sdk.ts` with the following content: + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, +}) +``` + +You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md). + +Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier. + +Create the file `src/admin/hooks/sanity.tsx` with the following content: + +```ts title="src/admin/hooks/sanity.tsx" +import { + useMutation, + UseMutationOptions, + useQueryClient, +} from "@tanstack/react-query" +import { sdk } from "../lib/sdk" + +export const useTriggerSanityProductSync = ( + id: string, + options?: UseMutationOptions +) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => + sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, { + method: "post", + }), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [`sanity_document`, `sanity_document_${id}`], + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} +``` + +You define the `useTriggerSanityProductSync` hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity. + +Add in the same file another hook: + +```ts title="src/admin/hooks/sanity.tsx" +// other imports... +import { + // ... + QueryKey, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" +import { FetchError } from "@medusajs/js-sdk" + +// ... + +export const useSanityDocument = ( + id: string, + query?: Record, + options?: Omit< + UseQueryOptions< + Record, + FetchError, + { sanity_document: Record; studio_url: string }, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const fetchSanityProductStatus = async (query?: Record) => { + return await sdk.client.fetch>( + `/admin/sanity/documents/${id}`, + { + query, + } + ) + } + + const { data, ...rest } = useQuery({ + queryFn: async () => fetchSanityProductStatus(query), + queryKey: [`sanity_document_${id}`], + ...options, + }) + + return { ...data, ...rest } +} +``` + +You define the hook `useSanityDocument` which retrieves the Sanity document of a product using Tankstack Query. + +You can now create the widget injected in a product's details page. Widgets are react components created in a file under the `src/admin/widgets` directory. + +So, create the file `src/admin/widgets/sanity-product.tsx` with the following content: + +```tsx title="src/admin/widgets/sanity-product.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { AdminProduct, DetailWidgetProps } from "@medusajs/types" +import { ArrowUpRightOnBox } from "@medusajs/icons" +import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui" +import { useState } from "react" +import { + useSanityDocument, + useTriggerSanityProductSync, +} from "../hooks/sanity" + +const ProductWidget = ({ data }: DetailWidgetProps) => { + const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id) + const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id) + const [showCodeBlock, setShowCodeBlock] = useState(false) + + const handleSync = async () => { + try { + await mutateAsync(undefined) + toast.success(`Sync triggered.`) + } catch (err) { + toast.error(`Couldn't trigger sync: ${ + (err as Record).message + }`) + } + } + + return ( + +
+
+

Sanity Status

+
+ {isLoading ? ( + "Loading..." + ) : sanity_document?.title === data.title ? ( + Synced + ) : ( + Not Synced + )} +
+
+ +
+
+
+ + {studio_url && ( + + + + )} +
+ {!isLoading && showCodeBlock && ( + + + + )} +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.after", +}) + +export default ProductWidget +``` + +The file exports a `ProductWidget` component and a `config` object created with `defineWidgetConfig` from the Admin Extension SDK. In the `config` object, you specify the zone to inject the widget into in the `zone` property. + +Find all widget injection zones in [this reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-widget-injection-zones/index.html.md). + +In the widget, you use the `useSanityDocument` to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data. + +When the "Sync" button is clicked, you use the `useTriggerSanityProductSync` hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async. + +To render a widget that matches the rest of the admin dashboard's design, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md), such as the `CodeBlock` or `Container` components. + +Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). + +### Test it Out + +To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page. + +You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity. + +*** + +## Step 12: Add Track Syncs Page to Medusa Admin + +Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the `sanitySyncProductsWorkflow` and their status. You'll also add the ability to sync all products to Sanity from that page. + +![A screenshot of the page to track and trigger syncs.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732095185/Medusa%20Resources/Screenshot_2024-11-20_at_11.09.42_AM_te8xic.png) + +### Retrieve Sync Executions API Route + +Medusa has a [workflow engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/index.html.md) that manages workflow executions, roll-backs, and other functionalities under the hood. + +The workflow engine is an [Infrastructure Module](https://docs.medusajs.com/docs/learn/fundamentals/modules/infrastructure-modules/index.html.md), which can be replaced with a [Redis Workflow Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md), or a custom one of your choice, allowing you to take ownership of your application's tooling. + +In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress. + +In this section, you'll create an API route to retrieve the stored executions of the `sanitySyncProductsWorkflow` workflow, so that you can display them later on the dashboard. + +When you defined the `sanitySyncProductsWorkflow`, you set its `retentionTime` option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking. + +Create the file `src/api/admin/sanity/syncs/route.ts` with the following content: + +```ts title="src/api/admin/sanity/syncs/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { + sanitySyncProductsWorkflow, +} from "../../../../workflows/sanity-sync-products" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const workflowEngine = req.scope.resolve( + Modules.WORKFLOW_ENGINE + ) + + const [executions, count] = await workflowEngine + .listAndCountWorkflowExecutions( + { + workflow_id: sanitySyncProductsWorkflow.getName(), + }, + { order: { created_at: "DESC" } } + ) + + res.json({ workflow_executions: executions, count }) +} +``` + +You add a `GET` API route at `/admin/sanity/syncs`. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the `listAndCountWorkflowExecutions` method to retrieve the executions of the `sanitySyncProductsWorkflow` workflow, filtering by its name. + +You return the executions in the JSON response of the route. + +### Trigger Sync API Route + +In this section, you'll add another API route that triggers syncing all products to Sanity. + +In the same file `src/api/admin/sanity/syncs/route.ts`, add the following: + +```ts title="api/admin/sanity/syncs/route.ts" +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({ + input: {}, + }) + + res.json({ transaction_id: transaction.transactionId }) +} +``` + +This adds a `POST` API route at `/admin/sanity/syncs`. In the route handler, you execute the `sanitySyncProductsWorkflow` without passing it a `product_ids` input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity. + +You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a [Get Execution API route](https://docs.medusajs.com/apiadmin#workflows-executions_getworkflowsexecutionsworkflow_idtransaction_id/index.html.md) that you can use to get the details of a workflow's execution. + +### Add Sanity UI Route + +In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of `sanitySyncProductsWorkflow` executions and allows triggering sync of all products in Medusa. + +A UI route is React component exported in a file under the `src/admin/routes` directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes. + +Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file `src/admin/hooks/sanity.tsx`, add the following two new hooks: + +```tsx title="src/admin/hooks/sanity.tsx" +export const useTriggerSanitySync = (options?: UseMutationOptions) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: () => + sdk.client.fetch(`/admin/sanity/syncs`, { + method: "post", + }), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: [`sanity_sync`], + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useSanitySyncs = ( + query?: Record, + options?: Omit< + UseQueryOptions< + Record, + FetchError, + { workflow_executions: Record[] }, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const fetchSanitySyncs = async (query?: Record) => { + return await sdk.client.fetch>(`/admin/sanity/syncs`, { + query, + }) + } + + const { data, ...rest } = useQuery({ + queryFn: async () => fetchSanitySyncs(query), + queryKey: [`sanity_sync`], + ...options, + }) + + return { ...data, ...rest } +} +``` + +The `useTriggerSanitySync` hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products. + +The `useSanitySyncs` hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections. + +Finally, to create the UI route, create the file `src/admin/routes/sanity/page.tsx` with the following content: + +```tsx title="src/admin/routes/sanity/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Sanity } from "@medusajs/icons" +import { + Badge, + Button, + Container, + Heading, + Table, + Toaster, + toast, +} from "@medusajs/ui" +import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity" + +const SanityRoute = () => { + const { mutateAsync, isPending } = useTriggerSanitySync() + const { workflow_executions, refetch } = useSanitySyncs() + + const handleSync = async () => { + try { + await mutateAsync() + toast.success(`Sync triggered.`) + refetch() + } catch (err) { + toast.error(`Couldn't trigger sync: ${ + (err as Record).message + }`) + } + } + + const getBadgeColor = (state: string) => { + switch (state) { + case "invoking": + return "blue" + case "done": + return "green" + case "failed": + return "red" + default: + return "grey" + } + } + + return ( + <> + +
+ + Sanity Syncs + + +
+ + + + Sync ID + Status + Created At + Updated At + + + + + {(workflow_executions || []).map((execution) => ( + + (window.location.href = `/app/sanity/${execution.id}`) + } + > + {execution.id} + + + {execution.state} + + + {execution.created_at} + {execution.updated_at} + + ))} + +
+
+ + + ) +} + +export const config = defineRouteConfig({ + label: "Sanity", + icon: Sanity, +}) + +export default SanityRoute +``` + +The file's path relative to the `src/admin/routes` directory indicates its path in the admin dashboard. So, this adds a new route at the path `http://localhost:9000/app/sanity`. + +The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with `defineRouteConfig` from the Admin Extension SDK. The function accepts the following properties: + +- `label`: The sidebar item's label. +- `icon`: The icon to the show in the sidebar. + +In the UI route, you use the `useSanitySyncs` hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the `useTriggerSanitySync` hook to send a request to the Medusa application and trigger the sync. + +To display components that match the design of the Medusa Admin, you use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). + +Learn more about UI routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). + +### Test it Out + +To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar. + +If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table. + +*** + +## Next Steps + +You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features. + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # Implement Localization in Medusa by Integrating Contentful In this tutorial, you'll learn how to localize your Medusa store's data with Contentful. @@ -50350,24 +53463,25 @@ If you encounter issues not covered in the troubleshooting guides: 3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. -# Integrate Medusa with Sanity (CMS) +# Integrate Medusa with ShipStation (Fulfillment) -In this guide, you'll learn how to integrate Medusa with Sanity. +In this guide, you'll learn how to integrate Medusa with ShipStation. -When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. The Medusa Framework supports you in integrating a CMS with these features. +Refer your technical team to this guide to integrate ShipStation with your Medusa application. You can then enable it using the Medusa Admin as explained in [this user guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md). -Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing. +When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. Medusa's [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md) provides fulfillment-related resources and functionalities in your store, but it delegates the processing and shipment of order fulfillments to providers that you can integrate. + +[ShipStation](https://shipstation.com/) is a shipping toolbox that connects all your shipping providers within one platform. By integrating it with Medusa, you can allow customers to choose from different providers like DHL and FedEx and view price rates retrieved from ShipStation. Admin users will also process the order fulfillment using the ShipStation integration. This guide will teach you how to: - Install and set up Medusa. -- Install and set up Sanity with Medusa's Next.js Starter storefront. -- Sync product data from Medusa to Sanity when a product is created or updated. -- Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity. +- Set up a ShipStation account. +- Integrate ShipStation as a fulfillment provider in Medusa. -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in [their documentation](https://www.sanity.io/docs). +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. -[Example Repository](https://github.com/medusajs/examples/tree/main/sanity-integration): Find the full code of the guide in this repository. +[Example Repository](https://github.com/medusajs/examples/tree/main/shipstation-integration): Find the full code of the guide in this repository. *** @@ -50399,395 +53513,1006 @@ Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednas *** -## Step 2: Install Sanity Client SDK +## Step 2: Prepare ShipStation Account -In this step, you'll install [Sanity's JavaScript client SDK](https://www.sanity.io/docs/js-client) in the Medusa application, which you'll use later in your code when sending requests to Sanity. +In this step, you'll prepare your ShipStation account before integrating it into Medusa. If you don't have an account, create one [here](https://www.shipstation.com/start-a-free-trial). -In your terminal, move to the Medusa application's directory and run the following command: +### Enable Carriers -```bash npm2yarn -cd project-name # replace with directory name -npm install @sanity/client -``` +To create labels for your shipments, you need to enable carriers. This requires you to enter payment and address details. + +To enable carriers: + +1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Enable Carriers" button. + +![Scroll down to the Enable carriers & see rates section, and find the "Enable Carriers" button.](https://res.cloudinary.com/dza7lstvk/image/upload/v1734523873/Medusa%20Resources/Screenshot_2024-12-18_at_2.10.54_PM_pmvcfr.png) + +2. In the pop-up that opens, click on Continue Setup. + +![Click on the green Continue Setup button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524261/Medusa%20Resources/Screenshot_2024-12-18_at_2.11.47_PM_wsl98i.png) + +3. In the next section of the form, you have to enter your payment details and billing address. Once done, click on Continue Setup. +4. After that, click the checkboxes on the Terms of Service section, then click the Finish Setup button. + +![Enable the two checkboxes, then click on Finish Setup at the bottom right](https://res.cloudinary.com/dza7lstvk/image/upload/v1734524486/Medusa%20Resources/Screenshot_2024-12-18_at_2.20.12_PM_pkixma.png) + +5. Once you're done, you can optionally add funds to your account. If you're not US-based, make sure to disable ParcelGuard insurance. Otherwise, an error will occur while retrieving rates later. + +### Add Carriers + +You must have at least one carrier (shipping provider) added in your ShipStation account. You'll later provide shipping options for each of these carriers in your Medusa application. + +To add carriers: + +1. On the Onboard page, in the "Enable carriers & see rates" section, click on the "Add your carrier accounts" link. + +![Scroll down to the Enable carriers & see rates section, and find the "Add your carrier accounts" link under the "Enable Carriers" button](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336612/Medusa%20Resources/Screenshot_2024-12-16_at_10.09.08_AM_nqshhg.png) + +2. Click on a provider from the pop-up window. + +![Click on the provider tiles in the pop-up window](https://res.cloudinary.com/dza7lstvk/image/upload/v1734336826/Medusa%20Resources/Screenshot_2024-12-16_at_10.13.37_AM_og4sdq.png) + +Based on the provider you chose, you'll have to enter your account details, then submit the form. + +### Activate Shipping API + +To integrate ShipStation using their API, you must enable the Shipping API Add-On. To do that: + +1. Go to Add-Ons from the navigation bar. +2. Find Shipping API and activate it. + +You'll later retrieve your API key. *** -## Step 3: Create a Sanity Project - -When the Medusa application connects to Sanity, it must connect to a project in Sanity. - -So, before building the integration in Medusa, create a project in Sanity using their website: - -1. [Sign in or sign up on the Sanity website.](https://www.sanity.io/login) -2. On your account's dashboard, click the "Create new project" button. - -![The Create new project button is at the top of the dashboard page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.31.10_AM_vvq7y6.png) - -3. Enter a project name and click "Create Project" - -![A pop-up form will open where you can choose project name and organization.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732091565/Medusa%20Resources/Screenshot_2024-11-20_at_10.32.33_AM_xb0rsn.png) - -You'll go back to the project's setting page in a later step. - -*** - -## Step 4: Create Sanity Module +## Step 3: Create ShipStation Module Provider To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. -In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it. +Medusa's Fulfillment Module delegates processing fulfillments and shipments to other modules, called module providers. In this step, you'll create a ShipStation Module Provider that implements all functionalities required for fulfillment. In later steps, you'll add into Medusa shipping options for ShipStation, and allow customers to choose it during checkout. Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). ### Create Module Directory -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/sanity`. +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/shipstation`. + +![The directory structure of the Medusa application after adding the module's directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1734338950/Medusa%20Resources/shipstation-dir-overview-1_dlsrbv.jpg) ### Create Service You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. -Medusa registers the module's service in the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), allowing you to easily resolve the service from other customizations and use its methods. +In this section, you'll create the ShipStation Module Provider's service and the methods necessary to handle fulfillment. -The Medusa application registers resources, such as a module's service or the [logging tool](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +Start by creating the file `src/modules/shipstation/service.ts` with the following content: -In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity. +![The directory structure of the Medusa application after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339042/Medusa%20Resources/shipstation-dir-overview-2_cmgvcj.jpg) -Start by creating the file `src/modules/sanity/service.ts` with the following content: +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights1} +import { AbstractFulfillmentProviderService } from "@medusajs/framework/utils" -```ts title="src/modules/sanity/service.ts" -import { - Logger, -} from "@medusajs/framework/types" -import { - SanityClient, -} from "@sanity/client" - -class SanityModuleService { - private client: SanityClient - private studioUrl?: string - private logger: Logger - - // TODO +export type ShipStationOptions = { + api_key: string } -export default SanityModuleService -``` +class ShipStationProviderService extends AbstractFulfillmentProviderService { + static identifier = "shipstation" + protected options_: ShipStationOptions -You create the `SanityModuleService` class that for now only has three properties: + constructor({}, options: ShipStationOptions) { + super() -- `client` property of type `SanityClient` (from the Sanity SDK you installed in the previous step) to send requests to Sanity. -- `studioUrl` property which will hold the URL to access the Sanity studio. -- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. + this.options_ = options + } -In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID. - -So, add after the import at the top of the file the following types: - -```ts title="src/modules/sanity/service.ts" -// other imports... - -const SyncDocumentTypes = { - PRODUCT: "product", -} as const - -type SyncDocumentTypes = - (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes]; - -type ModuleOptions = { - api_token: string; - project_id: string; - api_version: string; - dataset: "production" | "development"; - type_map?: Record; - studio_url?: string; + // TODO add methods } + +export default ShipStationProviderService ``` -The `ModuleOptions` type defines the type of options that the module expects: +A Fulfillment Module Provider service must extend the `AbstractFulfillmentProviderService` class. You'll implement the abstract methods of this class in the upcoming sections. -- `api_token`: API token to connect to Sanity. -- `project_id`: The ID of the Sanity project. -- `api_version`: The Sanity API version. -- `dataset`: The dataset to use, which is either `production` or `development`. -- `type_map`: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too. -- `studio_url`: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard. +The service must have an `identifier` static property, which is a unique identifier for the provider. You set the identifier to `shipstation`. -You can now initialize the client, which you'll do in the `constructor` of the `SanityModuleService`: +A module can receive options that are set when you later add the module to Medusa's configurations. These options allow you to safely store secret values outside of your code. -```ts title="src/modules/sanity/service.ts" -import { - // other imports... - createClient, -} from "@sanity/client" +The ShipStation module requires an `api_key` option, indicating your ShipStation's API key. You receive the options as a second parameter of the service's constructor. -// types... +### Create Client -type InjectedDependencies = { - logger: Logger -}; +To send requests to ShipStation, you'll create a client class that provides the methods to send requests. You'll then use that class in your service. -class SanityModuleService { - // properties... - constructor({ - logger, - }: InjectedDependencies, options: ModuleOptions) { - this.client = createClient({ - projectId: options.project_id, - apiVersion: options.api_version, - dataset: options.dataset, - token: options.api_token, +Create the file `src/modules/shipstation/client.ts` with the following content: + +![The directory structure of the Medusa application after adding the client file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734339519/Medusa%20Resources/shipstation-dir-overview-3_b8im2d.jpg) + +```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights1} +import { ShipStationOptions } from "./service" +import { MedusaError } from "@medusajs/framework/utils" + +export class ShipStationClient { + options: ShipStationOptions + + constructor(options) { + this.options = options + } + + private async sendRequest(url: string, data?: RequestInit): Promise { + return fetch(`https://api.shipstation.com/v2${url}`, { + ...data, + headers: { + ...data?.headers, + "api-key": this.options.api_key, + "Content-Type": "application/json", + }, + }).then((resp) => { + const contentType = resp.headers.get("content-type") + if (!contentType?.includes("application/json")) { + return resp.text() + } + + return resp.json() }) - this.logger = logger + .then((resp) => { + if (typeof resp !== "string" && resp.errors?.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An error occured while sending a request to ShipStation: ${ + resp.errors.map((error) => error.message) + }` + ) + } - this.logger.info("Connected to Sanity") - - this.studioUrl = options.studio_url - - // TODO initialize more properties + return resp + }) } } ``` -The service's constructor accepts two parameters: +The `ShipStationClient` class accepts the ShipStation options in its constructor and sets those options in the `options` property. -1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). -2. The options passed to the module. +You also add a private `sendRequest` method that accepts a path to send a request to and the request's configurations. In the method, you send a request using the Fetch API, passing the API key from the options in the request header. You also parse the response body based on its content type, and check if there are any errors to be thrown before returning the parsed response. -In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives. +You'll add more methods to send requests in the upcoming steps. -You also initialize the `logger` and `studioUrl` properties, and log a message indicating that connection to Sanity was successful. +To use the client in `ShipStationProviderService`, add it as a class property and initialize it in the constructor: -#### Transform Product Data +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights2} +// imports... +import { ShipStationClient } from "./client" -When you create or update products in Sanity, you must prepare the product object based on what Sanity expects. +// ... -So, you'll add methods to the service that transform a Medusa product to a Sanity document object. +class ShipStationProviderService extends AbstractFulfillmentProviderService { + // properties... + protected client: ShipStationClient -Start by adding the following types and class properties to `src/modules/sanity/service.ts`: - -```ts title="src/modules/sanity/service.ts" -type SyncDocumentInputs = T extends "product" - ? ProductDTO - : never - -type TransformationMap = Record< - SyncDocumentTypes, - (data: SyncDocumentInputs) => any ->; - -class SanityModuleService { - // other properties... - private typeMap: Record - private createTransformationMap: TransformationMap - private updateTransformationMap: TransformationMap - - // ... + constructor({}, options: ShipStationOptions) { + // ... + this.client = new ShipStationClient(options) + } } ``` -First, you define types for a transformation map, which is a map that pairs up a document type (such as `product`) to a function that handles transforming its data. +You import `ShipStationClient` and add a new `client` property in `ShipStationProviderService`. In the class's constructor, you set the `client` property by initializing `ShipStationProviderService`, passing it the module's options. -Then, in the service, you define three new properties: +You'll use the `client` property when implementing the service's methods. -- `typeMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and their type name in Sanity. -- `createTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity document data to be created. -- `updateTransformationMap`: Pair of `SyncDocumentTypes` values (for example, `product`) and the method used to transform a Medusa product to a Sanity update operation. +### Implement Service Methods -Next, add the following two methods to transform a product: +In this section, you'll go back to the `ShipStationProviderService` method to implement the abstract methods of `AbstractFulfillmentProviderService`. -```ts title="src/modules/sanity/service.ts" +Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) for a full reference of all methods, their parameters and return types. + +#### getFulfillmentOptions + +The `getFulfillmentOptions` method returns the options that this fulfillment provider supports. When admin users add shipping options later in the Medusa Admin, they'll select one of these options. + +Learn more about shipping options in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md). + +ShipStation requires that a shipment must be associated with a carrier and one of its services. So, in this method, you'll retrieve the list of carriers from ShipStation and return them as fulfillment options. Shipping options created from these fulfillment options will always have access to the option's carrier and service. + +Before you start implementing methods, you'll add the expected carrier types returned by ShipStation. Create the file `src/modules/shipstation/types.ts` with the following content: + +![The directory structure of the Medusa application after adding the types file](https://res.cloudinary.com/dza7lstvk/image/upload/v1734340402/Medusa%20Resources/shipstation-dir-overview-4_fwsle0.jpg) + +```ts title="src/modules/shipstation/types.ts" +export type Carrier = { + carrier_id: string + disabled_by_billing_plan: boolean + friendly_name: string + services: { + service_code: string + name: string + }[] + packages: { + package_code: string + }[] + [k: string]: unknown +} + +export type CarriersResponse = { + carriers: Carrier[] +} +``` + +You define a `Carrier` type that holds a carrier's details, and a `CarriersResponse` type, which is the response returned by ShipStation. + +A carrier has more fields that you can use. Refer to [ShipStation's documentation](https://docs.shipstation.com/openapi/carriers/list_carriers#carriers/list_carriers/t=response\&c=200\&path=carriers) for all carrier fields. + +Next, you'll add in `ShipStationClient` the method to retrieve the carriers from ShipStation. So, add to the class defined in `src/modules/shipstation/client.ts` a new method: + +```ts title="src/modules/shipstation/client.ts" highlights={clientHighlights2} // other imports... -import { - ProductDTO, +import { + CarriersResponse, +} from "./types" + +export class ShipStationClient { + // ... + async getCarriers(): Promise { + return await this.sendRequest("/carriers") + } +} +``` + +You added a new `getCarriers` method that uses the `sendRequest` method to send a request to the [ShipStation's List Carriers endpoint](https://docs.shipstation.com/openapi/carriers/list_carriers). The method returns `CarriersResponse` that you defined earlier. + +Finally, add the `getFulfillmentOptions` method to `ShipStationProviderService`: + +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights3} +// other imports... +import { + FulfillmentOption, } from "@medusajs/framework/types" -class SanityModuleService { +class ShipStationProviderService extends AbstractFulfillmentProviderService { // ... - private transformProductForCreate = (product: ProductDTO) => { - return { - _type: this.typeMap[SyncDocumentTypes.PRODUCT], - _id: product.id, - title: product.title, - specs: [ - { - _key: product.id, - _type: "spec", - title: product.title, - lang: "en", - }, - ], - } - } + async getFulfillmentOptions(): Promise { + const { carriers } = await this.client.getCarriers() + const fulfillmentOptions: FulfillmentOption[] = [] - private transformProductForUpdate = (product: ProductDTO) => { - return { - set: { - title: product.title, - }, - } + carriers + .filter((carrier) => !carrier.disabled_by_billing_plan) + .forEach((carrier) => { + carrier.services.forEach((service) => { + fulfillmentOptions.push({ + id: `${carrier.carrier_id}__${service.service_code}`, + name: service.name, + carrier_id: carrier.carrier_id, + carrier_service_code: service.service_code, + }) + }) + }) + + return fulfillmentOptions } } ``` -The `transformProductForCreate` method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the `transformProductForUpdate` method accepts a product and returns an object that you'll later pass to Sanity to update the product document. +In the `getFulfillmentOptions` method, you retrieve the carriers from ShipStation. You then filter out the carriers disabled by your ShipStation billing plan, and loop over the remaining carriers and their services. -The Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods. +You return an array of fulfillment-option objects, where each object represents a carrier and service pairing. Each object has the following properties: -Finally, initialize the new properties you added in the `SanityModuleService`'s constructor: +- an `id` property, which you set to a combination of the carrier ID and the service code. +- a `name` property, which you set to the service's `name`. The admin user will see this name when they create a shipping option for the ShipStation provider. +- You can pass other data, such as `carrier_id` and `carrier_service_code`, and Medusa will store the fulfillment option in the `data` property of shipping options created later. -```ts title="src/modules/sanity/service.ts" -class SanityModuleService { - // ... - constructor({ - logger, - }: InjectedDependencies, options: ModuleOptions) { - // ... - this.typeMap = Object.assign( - {}, - { - [SyncDocumentTypes.PRODUCT]: "product", - }, - options.type_map || {} - ) +Learn more about the shipping option's `data` property in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md). - this.createTransformationMap = { - [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate, - } +You'll see this method in action later when you create a shipping option. - this.updateTransformationMap = { - [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate, - } - } - // ... -} -``` +#### canCalculate -You initialize the `typeMap` property to map the `product` type in Medusa to the `product` schema type in Sanity. You also initialize the `createTransformationMap` and `updateTransformationMap` to map the methods to transform a product for creation or update. +When an admin user creates a shipping option for your provider, they can choose whether the price is flat rate or calculated during checkout. -You can modify these properties to add support for other schema types, such as product categories or collections. +If the user chooses calculated, Medusa validates that your fulfillment provider supports calculated prices using the `canCalculate` method of your provider's service. -#### Methods to Manage Documents +This method accepts the shipping option's `data` field, which will hold the data of an option returned by `getFulfillmentOptions`. It returns a boolean value indicating whether the shipping option can have a calculated price. -In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity. +Add the method to `ShipStationProviderService` in `src/modules/shipstation/service.ts`: -Add the following methods to the `SanityModuleService` class: - -```ts title="src/modules/sanity/service.ts" highlights={syncMethodsHighlights} +```ts title="src/modules/shipstation/service.ts" // other imports... import { // ... - FirstDocumentMutationOptions, -} from "@sanity/client" + CreateShippingOptionDTO, +} from "@medusajs/framework/types" -class SanityModuleService { +class ShipStationProviderService extends AbstractFulfillmentProviderService { // ... - async upsertSyncDocument( - type: T, - data: SyncDocumentInputs - ) { - const existing = await this.client.getDocument(data.id) - if (existing) { - return await this.updateSyncDocument(type, data) - } - - return await this.createSyncDocument(type, data) - } - - async createSyncDocument( - type: T, - data: SyncDocumentInputs, - options?: FirstDocumentMutationOptions - ) { - const doc = this.createTransformationMap[type](data) - return await this.client.create(doc, options) - } - - async updateSyncDocument( - type: T, - data: SyncDocumentInputs - ) { - const operations = this.updateTransformationMap[type](data) - return await this.client.patch(data.id, operations).commit() + async canCalculate(data: CreateShippingOptionDTO): Promise { + return true } } ``` -You add three methods: +Since all shipping option prices can be calculated with ShipStation based on the chosen carrier and service zone, you always return `true` in this method. -- `upsertSyncDocument`: Creates or updates a document in Sanity for a data type in Medusa. -- `createSyncDocument`: Creates a document in Sanity for a data type in Medusa. It uses the `createTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). -- `updateSyncDocument`: Updates a document in Sanity for a data type in Medusa. It uses the `updateTransformationMap` property to use the transform method of the specified Medusa data type (for example, a product's data). +You'll implement the calculation mechanism in a later method. -You also need methods to manage the Sanity documents without transformations. So, add the following methods to `SanityModuleService`: +#### calculatePrice -```ts title="src/modules/sanity/service.ts" highlights={methodsHighlights} -class SanityModuleService { - // ... - async retrieve(id: string) { - return this.client.getDocument(id) +When the customer views available shipping options during checkout, the Medusa application requests the calculated price from your fulfillment provider using its `calculatePrice` method. + +To retrieve shipping prices with ShipStation, you create a shipment first then get its rates. So, in the `calculatePrice` method, you'll either: + +- Send a request to [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates) that creates a shipment and returns its prices; +- Or, if a shipment was already created before, you'll retrieve its prices using [ShipStation's get shipment rates endpoint](https://docs.shipstation.com/openapi/shipments/list_shipment_rates). + +First, add the following types to `src/modules/shipstation/types.ts`: + +```ts title="src/modules/shipstation/types.ts" highlights={typesHighlights1} +export type ShipStationAddress = { + name: string + phone: string + email?: string | null + company_name?: string | null + address_line1: string + address_line2?: string | null + address_line3?: string | null + city_locality: string + state_province: string + postal_code: string + country_code: string + address_residential_indicator: "unknown" | "yes" | "no" + instructions?: string | null + geolocation?: { + type?: string + value?: string + }[] +} + +export type Rate = { + rate_id: string + shipping_amount: { + currency: string + amount: number } - - async delete(id: string) { - return this.client.delete(id) + insurance_amount: { + currency: string + amount: number } - - async update(id: string, data: any) { - return await this.client.patch(id, { - set: data, - }).commit() + confirmation_amount: { + currency: string + amount: number } + other_amount: { + currency: string + amount: number + } + tax_amount: { + currency: string + amount: number + } +} - async list( - filter: { - id: string | string[] +export type RateResponse = { + rates: Rate[] +} + +export type GetShippingRatesRequest = { + shipment_id?: string + shipment?: Omit + rate_options: { + carrier_ids: string[] + service_codes: string[] + preferred_currency: string + } +} + +export type GetShippingRatesResponse = { + shipment_id: string + carrier_id?: string + service_code?: string + external_order_id?: string + rate_response: RateResponse +} + +export type Shipment = { + shipment_id: string + carrier_id: string + service_code: string + ship_to: ShipStationAddress + return_to?: ShipStationAddress + is_return?: boolean + ship_from: ShipStationAddress + items?: [ + { + name?: string + quantity?: number + sku?: string } - ) { - const data = await this.client.getDocuments( - Array.isArray(filter.id) ? filter.id : [filter.id] - ) + ] + warehouse_id?: string + shipment_status: "pending" | "processing" | "label_purchased" | "cancelled" + [k: string]: unknown +} - return data.map((doc) => ({ - id: doc?._id, - ...doc, - })) +``` + +You add the following types: + +- `ShipStationAddress`: an address to ship from or to. +- `Rate`: a price rate for a specified carrier and service zone. +- `RateResponse`: The response when retrieving rates. +- `GetShippingRatesRequest`: The request body data for [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other accepted parameters. +- `GetShippingRatesResponse`: The response of the [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates). You can refer to their API reference for other response fields. +- `Shipment`: A shipment's details. + +Then, add the following methods to `ShipStationClient`: + +```ts title="src/modules/shipstation/client.ts" highlights={serviceHighlights7} +// other imports... +import { + // ... + GetShippingRatesRequest, + GetShippingRatesResponse, + RateResponse, +} from "./types" + +export class ShipStationClient { + // ... + async getShippingRates( + data: GetShippingRatesRequest + ): Promise { + return await this.sendRequest("/rates", { + method: "POST", + body: JSON.stringify(data), + }).then((resp) => { + if (resp.rate_response.errors?.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `An error occured while retrieving rates from ShipStation: ${ + resp.rate_response.errors.map((error) => error.message) + }` + ) + } + + return resp + }) + } + + async getShipmentRates(id: string): Promise { + return await this.sendRequest(`/shipments/${id}/rates`) } } ``` -You add other three methods: +The `getShippingRates` method accepts as a parameter the data to create a shipment and retrieve its rate. In the method, you send the request using the `sendRequest` method, and throw any errors in the rate retrieval before returning the response. -- `retrieve` to retrieve a document by its ID. -- `delete` to delete a document by its ID. -- `update` to update a document by its ID with new data. -- `list` to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents. +The `getShipmentRates` method accepts the ID of the shipment as a parameter, sends the request using the `sendRequest` method and returns its response holding the shipment's rates. -### Export Module Definition +Next, add to `ShipStationProviderService` a private method that'll be used to create a shipment in ShipStation and get its rates: -The `SanityModuleService` class now has the methods necessary to connect to and perform actions in Sanity. +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights8} +// other imports... +import { + // ... + MedusaError, +} from "@medusajs/framework/utils" +import { + // ... + CalculateShippingOptionPriceDTO, +} from "@medusajs/framework/types" +import { + GetShippingRatesResponse, + ShipStationAddress, +} from "./types" -Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service. +class ShipStationProviderService extends AbstractFulfillmentProviderService { + // ... + private async createShipment({ + carrier_id, + carrier_service_code, + from_address, + to_address, + items, + currency_code, + }: { + carrier_id: string + carrier_service_code: string + from_address?: { + name?: string + address?: Omit< + StockLocationAddressDTO, "created_at" | "updated_at" | "deleted_at" + > + }, + to_address?: Omit< + CartAddressDTO, "created_at" | "updated_at" | "deleted_at" | "id" + >, + items: CartLineItemDTO[] | OrderLineItemDTO[], + currency_code: string + }): Promise { + if (!from_address?.address) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "from_location.address is required to calculate shipping rate" + ) + } + const ship_from: ShipStationAddress = { + name: from_address?.name || "", + phone: from_address?.address?.phone || "", + address_line1: from_address?.address?.address_1 || "", + city_locality: from_address?.address?.city || "", + state_province: from_address?.address?.province || "", + postal_code: from_address?.address?.postal_code || "", + country_code: from_address?.address?.country_code || "", + address_residential_indicator: "unknown", + } + if (!to_address) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "shipping_address is required to calculate shipping rate" + ) + } + + const ship_to: ShipStationAddress = { + name: `${to_address.first_name} ${to_address.last_name}`, + phone: to_address.phone || "", + address_line1: to_address.address_1 || "", + city_locality: to_address.city || "", + state_province: to_address.province || "", + postal_code: to_address.postal_code || "", + country_code: to_address.country_code || "", + address_residential_indicator: "unknown", + } -Create the file `src/modules/sanity/index.ts` with the following content: + // TODO create shipment + } +} +``` -```ts title="src/modules/sanity/index.ts" -import { Module } from "@medusajs/framework/utils" -import SanityModuleService from "./service" +The `createShipment` method accepts as a parameter an object having the following properties: -export const SANITY_MODULE = "sanity" +- `carrier_id`: The ID of the carrier to create the shipment for. +- `carrier_service_code`: The code of the carrier's service. +- `from_address`: The address to ship items from, which is the address of the stock location associated with a shipping option. +- `to_address`: The address to ship items to, which is the customer's address. +- `items`: An array of the items in the cart or order (for fulfilling the order later). +- `currency_code`: The currency code of the cart or order. -export default Module(SANITY_MODULE, { - service: SanityModuleService, +In the `createShipment` method, so far you only prepare the data to be sent to ShipStation. ShipStation requires the addresses to ship the items from and to. + +To send the request, replace the `TODO` with the following: + +```ts title="src/modules/shipstation/service.ts" +// Sum the package's weight +// You can instead create different packages for each item +const packageWeight = items.reduce((sum, item) => { + // @ts-ignore + return sum + (item.variant.weight || 0) +}, 0) + +return await this.client.getShippingRates({ + shipment: { + carrier_id: carrier_id, + service_code: carrier_service_code, + ship_to, + ship_from, + validate_address: "no_validation", + items: items?.map((item) => ({ + name: item.title, + quantity: item.quantity, + sku: item.variant_sku || "", + })), + packages: [{ + weight: { + value: packageWeight, + unit: "kilogram", + }, + }], + customs: { + contents: "merchandise", + non_delivery: "return_to_sender", + }, + }, + rate_options: { + carrier_ids: [carrier_id], + service_codes: [carrier_service_code], + preferred_currency: currency_code as string, + }, }) ``` -In the file, you export the `SANITY_MODULE` which is the Module's name. You'll use it later when you resolve the module from the Medusa container. +You create a shipment and get its rates using the `getShippingRates` method you added to the client. You pass the method the expected request body parameters by [ShipStation's get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), including the carrier ID, the items to be shipped, and more. -You also export the module definition using `Module` from the Modueles SDK, which accepts as a first parameter the module's name, and as a second parameter an object having a `service` property, indicating the module's service. +The above snippet assumes all items are sent in a single package. You can instead pass a package for each item, specifying its weight and optionally its height, width, and length. + +Finally, add the `calculatePrice` method to `ShipStationProviderService`: + +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights5} +// other imports... +import { + // ... + CalculatedShippingOptionPrice, +} from "@medusajs/framework/types" + +class ShipStationProviderService extends AbstractFulfillmentProviderService { + // ... + async calculatePrice( + optionData: CalculateShippingOptionPriceDTO["optionData"], + data: CalculateShippingOptionPriceDTO["data"], + context: CalculateShippingOptionPriceDTO["context"] + ): Promise { + const { shipment_id } = data as { + shipment_id?: string + } || {} + const { carrier_id, carrier_service_code } = optionData as { + carrier_id: string + carrier_service_code: string + } + let rate: Rate | undefined + + if (!shipment_id) { + const shipment = await this.createShipment({ + carrier_id, + carrier_service_code, + from_address: { + name: context.from_location?.name, + address: context.from_location?.address, + }, + to_address: context.shipping_address, + items: context.items || [], + currency_code: context.currency_code as string, + }) + rate = shipment.rate_response.rates[0] + } else { + const rateResponse = await this.client.getShipmentRates(shipment_id) + rate = rateResponse[0].rates[0] + } + + const calculatedPrice = !rate ? 0 : rate.shipping_amount.amount + rate.insurance_amount.amount + + rate.confirmation_amount.amount + rate.other_amount.amount + + (rate.tax_amount?.amount || 0) + + return { + calculated_amount: calculatedPrice, + is_calculated_price_tax_inclusive: !!rate?.tax_amount, + } + } +} +``` + +The `calculatePrice` method accepts the following parameters: + +1. The `data` property of the chosen shipping option during checkout. +2. The `data` property of the shipping method, which will hold the ID of the shipment in ShipStation. +3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more. + +In the method, you first check if a `shipment_id` is already stored in the shipping method's `data` property. If so, you retrieve the shipment's rates using the client's `getShipmentRates` method. Otherwise, you use the `createShipment` method to create the shipment and get its rates. + +A rate returned by ShipStation has four properties that, when added up, make up the full price: `shipping_amount`, `insurance_amount`, `confirmation_amount`, and `other_amount`. It may have a `tax_amount` property, which is the amount for applied taxes. + +Learn more about these fields in [ShipStation's documentation](https://docs.shipstation.com/rate-shopping#about-the-response). + +The method returns an object having the following properties: + +- `calculated_amount`: The shipping method's price calculated by adding the four rate properties with the tax property, if available. +- `is_calculated_price_tax_inclusive`: Whether the price includes taxes, which is inferred from whether the `tax_amount` property is set in the rate. + +Customers will now see the calculated price of a ShipStation shipping option during checkout. + +#### validateFulfillmentData + +When a customer chooses a shipping option during checkout, Medusa creates a shipping method from that option. A shipping method has a `data` property to store data relevant for later processing of the method and its fulfillments. + +So, in the `validateFulfillmentData` method of your provider, you'll create a shipment in ShipStation if it wasn't already created using their [get shipping rates endpoint](https://docs.shipstation.com/openapi/rates/calculate_rates), and store the ID of that shipment in the created shipping method's `data` property. + +Add the `validateFulfillmentData` method to `ShipStationProviderService`: + +```ts title="src/modules/shipstation/service.ts" highlights={serviceHighlights4} +class ShipStationProviderService extends AbstractFulfillmentProviderService { + // ... + async validateFulfillmentData( + optionData: Record, + data: Record, + context: Record + ): Promise { + let { shipment_id } = data as { + shipment_id?: string + } + + if (!shipment_id) { + const { carrier_id, carrier_service_code } = optionData as { + carrier_id: string + carrier_service_code: string + } + const shipment = await this.createShipment({ + carrier_id, + carrier_service_code, + from_address: { + // @ts-ignore + name: context.from_location?.name, + // @ts-ignore + address: context.from_location?.address, + }, + // @ts-ignore + to_address: context.shipping_address, + // @ts-ignore + items: context.items || [], + // @ts-ignore + currency_code: context.currency_code, + }) + shipment_id = shipment.shipment_id + } + + return { + ...data, + shipment_id, + } + } +} +``` + +The `validateFulfillmentData` method accepts the following parameters: + +1. The `data` property of the chosen shipping option during checkout. It will hold the carrier ID and its service code. +2. The `data` property of the shipping method to be created. This can hold custom data sent in the [Add Shipping Method API route](https://docs.medusajs.com/api/store#carts_postcartsidshippingmethods). +3. An object of the checkout's context, including the cart's items, the location associated with the shipping option, and more. + +In the method, you try to retrieve the shipment ID from the shipping method's `data` parameter if it was already created. If not, you create the shipment in ShipStation using the `createShipment` method. + +Finally, you return the object to be stored in the shipping method's `data` property. You include in it the ID of the shipment in ShipStation. + +#### createFulfillment + +After the customer places the order, the admin user can manage its fulfillments. When the admin user creates a fulfillment for the order, Medusa uses the `createFulfillment` method of the associated provider to handle any processing in the third-party provider. + +This method supports creating split fulfillments, meaning you can partially fulfill and order's items. So, you'll create a new shipment, then purchase a label for that shipment. You'll use the existing shipment to retrieve details like the address to ship from and to. + +First, add a new type to `src/modules/shipstation/types.ts`: + +```ts title="src/modules/shipstation/types.ts" +export type Label = { + label_id: string + status: "processing" | "completed" | "error" | "voided" + shipment_id: string + ship_date: Date + shipment_cost: { + currency: string + amount: number + } + insurance_cost: { + currency: string + amount: number + } + confirmation_amount: { + currency: string + amount: number + } + tracking_number: string + is_return_label: boolean + carrier_id: string + service_code: string + trackable: string + tracking_status: "unknown" | "in_transit" | "error" | "delivered" + label_download: { + href: string + pdf: string + png: string + zpl: string + } +} +``` + +You add the `Label` type for the details in a label object. You can find more properties in [ShipStation's documentation](https://docs.shipstation.com/openapi/labels/create_label#labels/create_label/response\&c=200/body). + +Then, add the following methods to the `ShipStationClient`: + +```ts title="src/modules/shipstation/client.ts" +// other imports... +import { + // ... + Label, + Shipment, +} from "./types" + +export class ShipStationClient { + // ... + + async getShipment(id: string): Promise { + return await this.sendRequest(`/shipments/${id}`) + } + + async purchaseLabelForShipment(id: string): Promise
@@ -2805,11 +2807,11 @@ export const removeBundleFromCartWorkflowHighlights = [ import { createWorkflow, transform, - WorkflowResponse + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { deleteLineItemsWorkflow, - useQueryGraphStep + useQueryGraphStep, } from "@medusajs/medusa/core-flows" type RemoveBundleFromCartWorkflowInput = { @@ -2831,7 +2833,7 @@ export const removeBundleFromCartWorkflow = createWorkflow( }, options: { throwIfKeyNotFound: true, - } + }, }) const itemsToRemove = transform({ @@ -2847,7 +2849,7 @@ export const removeBundleFromCartWorkflow = createWorkflow( input: { cart_id, ids: itemsToRemove, - } + }, }) // retrieve cart again @@ -2886,10 +2888,10 @@ Next, you'll create the API route that exposes the workflow's functionality to s Create the file `src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts` with the following content: ```ts title="src/api/store/carts/[id]/line-item-bundles/[bundle_id]/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"; +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" import { - removeBundleFromCartWorkflow -} from "../../../../../../workflows/remove-bundle-from-cart"; + removeBundleFromCartWorkflow, +} from "../../../../../../workflows/remove-bundle-from-cart" export async function DELETE( req: MedusaRequest, @@ -2900,11 +2902,11 @@ export async function DELETE( input: { cart_id: req.params.id, bundle_id: req.params.bundle_id, - } + }, }) res.json({ - cart + cart, }) } ``` diff --git a/www/apps/resources/components/EventHeader/index.tsx b/www/apps/resources/components/EventHeader/index.tsx new file mode 100644 index 0000000000..fabdbf0aa3 --- /dev/null +++ b/www/apps/resources/components/EventHeader/index.tsx @@ -0,0 +1,78 @@ +"use client" + +import { Brackets, CheckCircle, SquareTwoStack, Tag } from "@medusajs/icons" +import { + DropdownMenu, + H2, + H2Props, + H3, + H3Props, + useCopy, + useGenerateSnippet, +} from "docs-ui" + +type EventHeaderProps = ( + | { + headerLvl: "2" + headerProps: H2Props + } + | { + headerLvl: "3" + headerProps: H3Props + } +) & { + eventName: string + payload: string +} + +export const EventHeader = ({ + headerLvl, + headerProps, + eventName, + payload: payloadStr, +}: EventHeaderProps) => { + const Header = headerLvl === "2" ? H2 : H3 + const { snippet } = useGenerateSnippet({ + type: "subscriber", + options: { + event: eventName, + payload: payloadStr, + }, + }) + const { handleCopy: handleEventNameCopy, isCopied: eventNameCopied } = + useCopy(eventName) + const { handleCopy: handleSnippetCopy, isCopied: snippetCopied } = + useCopy(snippet) + + return ( +
+
{eventName}
+ + {eventNameCopied || snippetCopied ? ( + + ) : ( + + )} + + } + menuItems={[ + { + type: "action", + title: "Copy event name", + action: () => handleEventNameCopy(), + icon: , + }, + { + type: "action", + title: "Copy subscriber for event", + action: () => handleSnippetCopy(), + icon: , + }, + ]} + menuClassName="z-10" + /> +
+ ) +} diff --git a/www/apps/resources/components/MDXComponents/index.tsx b/www/apps/resources/components/MDXComponents/index.tsx index b3e868e8db..be60b65ef5 100644 --- a/www/apps/resources/components/MDXComponents/index.tsx +++ b/www/apps/resources/components/MDXComponents/index.tsx @@ -10,8 +10,10 @@ import { Table, Badge, Tooltip, + CopyGeneratedSnippetButton, } from "docs-ui" import { CommerceModuleSections } from "../CommerceModuleSections" +import { EventHeader } from "../EventHeader" const MDXComponents: MDXComponentsType = { ...UiMdxComponents, @@ -27,6 +29,8 @@ const MDXComponents: MDXComponentsType = { Tooltip: (props) => { return }, + EventHeader, + CopyGeneratedSnippetButton, } export default MDXComponents diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index dbb6f714df..5389ec60fa 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -227,7 +227,7 @@ export const generatedEditDates = { "references/core_flows/Order/functions/core_flows.Order.updateOrderEditAddItemValidationStep/page.mdx": "2024-08-20T00:10:59.065Z", "references/core_flows/Order/functions/core_flows.Order.updateOrderEditAddItemWorkflow/page.mdx": "2024-08-20T00:10:59.105Z", "references/core_flows/core_flows.Order/page.mdx": "2025-04-23T16:21:19.028Z", - "references/modules/core_flows/page.mdx": "2025-05-01T15:18:36.397Z", + "references/modules/core_flows/page.mdx": "2025-05-07T15:35:08.516Z", "references/types/types.HttpTypes/page.mdx": "2025-04-11T09:04:46.305Z", "app/troubleshooting/medusa-admin/no-widget-route/page.mdx": "2025-03-11T08:57:17.255Z", "references/auth/IAuthModuleService/methods/auth.IAuthModuleService.createProviderIdentities/page.mdx": "2025-04-11T09:04:43.772Z", @@ -252,34 +252,34 @@ export const generatedEditDates = { "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.createRemoteLinkStep/page.mdx": "2025-04-11T09:04:35.926Z", "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.dismissRemoteLinkStep/page.mdx": "2025-04-11T09:04:35.929Z", "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.updateRemoteLinksStep/page.mdx": "2025-04-11T09:04:35.941Z", - "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.batchLinksWorkflow/page.mdx": "2025-05-01T15:18:36.716Z", - "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.createLinksWorkflow/page.mdx": "2025-05-01T15:18:36.718Z", - "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.dismissLinksWorkflow/page.mdx": "2025-05-01T15:18:36.720Z", - "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.updateLinksWorkflow/page.mdx": "2025-05-01T15:18:36.723Z", + "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.batchLinksWorkflow/page.mdx": "2025-05-07T15:35:08.905Z", + "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.createLinksWorkflow/page.mdx": "2025-05-07T15:35:08.909Z", + "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.dismissLinksWorkflow/page.mdx": "2025-05-07T15:35:08.913Z", + "references/core_flows/Common/Workflows_Common/functions/core_flows.Common.Workflows_Common.updateLinksWorkflow/page.mdx": "2025-05-07T15:35:08.916Z", "references/core_flows/Common/core_flows.Common.Steps_Common/page.mdx": "2024-12-23T12:30:23.689Z", "references/core_flows/Common/core_flows.Common.Workflows_Common/page.mdx": "2024-12-23T12:30:23.702Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createFulfillmentStep/page.mdx": "2025-04-11T09:04:36.222Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createReturnFulfillmentStep/page.mdx": "2025-04-11T09:04:36.246Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.updateFulfillmentStep/page.mdx": "2025-01-13T18:05:49.603Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.upsertShippingOptionsStep/page.mdx": "2025-04-11T09:04:36.337Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createFulfillmentWorkflow/page.mdx": "2025-05-01T15:18:37.214Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createReturnFulfillmentWorkflow/page.mdx": "2025-05-01T15:18:37.221Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShipmentWorkflow/page.mdx": "2025-05-01T15:18:37.233Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateFulfillmentWorkflow/page.mdx": "2025-05-01T15:18:37.270Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createFulfillmentWorkflow/page.mdx": "2025-05-07T15:35:09.511Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createReturnFulfillmentWorkflow/page.mdx": "2025-05-07T15:35:09.518Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShipmentWorkflow/page.mdx": "2025-05-07T15:35:09.530Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateFulfillmentWorkflow/page.mdx": "2025-05-07T15:35:09.566Z", "references/core_flows/Fulfillment/core_flows.Fulfillment.Steps_Fulfillment/page.mdx": "2024-12-23T12:30:23.835Z", "references/core_flows/Fulfillment/core_flows.Fulfillment.Workflows_Fulfillment/page.mdx": "2024-12-23T12:30:23.905Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createCompleteReturnStep/page.mdx": "2025-04-11T09:04:37.117Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderChangeStep/page.mdx": "2025-04-11T09:04:36.949Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderClaimsStep/page.mdx": "2025-04-23T16:21:19.177Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderClaimsStep/page.mdx": "2025-05-07T15:35:09.738Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderExchangesStep/page.mdx": "2025-04-11T09:04:37.029Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createReturnsStep/page.mdx": "2025-04-11T09:04:37.146Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.previewOrderChangeStep/page.mdx": "2025-04-11T09:04:37.071Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderChangesStep/page.mdx": "2025-05-01T15:18:37.614Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginClaimOrderWorkflow/page.mdx": "2025-05-01T15:18:37.706Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginExchangeOrderWorkflow/page.mdx": "2025-05-01T15:18:38.162Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginOrderEditOrderWorkflow/page.mdx": "2025-05-01T15:18:38.449Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginReceiveReturnWorkflow/page.mdx": "2025-05-01T15:18:38.706Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginReturnOrderWorkflow/page.mdx": "2025-05-01T15:18:38.726Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderChangesStep/page.mdx": "2025-05-07T15:35:09.906Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginClaimOrderWorkflow/page.mdx": "2025-05-07T15:35:09.993Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginExchangeOrderWorkflow/page.mdx": "2025-05-07T15:35:10.448Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginOrderEditOrderWorkflow/page.mdx": "2025-05-07T15:35:10.723Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginReceiveReturnWorkflow/page.mdx": "2025-05-07T15:35:10.984Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.beginReturnOrderWorkflow/page.mdx": "2025-05-07T15:35:11.005Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderClaimValidationStep/page.mdx": "2025-04-11T09:04:37.400Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderEditValidationStep/page.mdx": "2025-04-11T09:04:38.789Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderExchangeValidationStep/page.mdx": "2025-04-11T09:04:38.253Z", @@ -300,85 +300,85 @@ export const generatedEditDates = { "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.confirmReturnRequestValidationStep/page.mdx": "2025-04-11T09:04:39.503Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.confirmReturnRequestWorkflow/page.mdx": "2025-05-01T15:18:38.810Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createClaimShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:37.638Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createClaimShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:37.856Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createClaimShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:10.141Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createExchangeShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:38.334Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createExchangeShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.233Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderChangeWorkflow/page.mdx": "2025-05-01T15:18:38.059Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createExchangeShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:10.518Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderChangeWorkflow/page.mdx": "2025-05-07T15:35:10.350Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderEditShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:38.856Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderEditShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.507Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderFulfillmentWorkflow/page.mdx": "2025-05-01T15:18:38.028Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderShipmentWorkflow/page.mdx": "2025-05-01T15:18:38.131Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrdersWorkflow/page.mdx": "2025-05-01T15:18:38.114Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrdersWorkflow/page.mdx": "2025-05-07T15:35:10.401Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createReturnShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:39.584Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createReturnShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.855Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createReturnShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:11.127Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.dismissItemReturnRequestValidationStep/page.mdx": "2025-04-11T09:04:39.627Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.dismissItemReturnRequestWorkflow/page.mdx": "2025-05-01T15:18:38.878Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.dismissItemReturnRequestWorkflow/page.mdx": "2025-05-07T15:35:11.148Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.exchangeAddNewItemValidationStep/page.mdx": "2025-04-11T09:04:38.385Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.exchangeRequestItemReturnValidationStep/page.mdx": "2025-04-23T16:21:19.973Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.getOrdersListWorkflow/page.mdx": "2025-05-01T15:18:38.414Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.getOrdersListWorkflow/page.mdx": "2025-05-07T15:35:10.688Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimAddNewItemValidationStep/page.mdx": "2025-04-11T09:04:37.433Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimAddNewItemWorkflow/page.mdx": "2025-05-01T15:18:37.753Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimAddNewItemWorkflow/page.mdx": "2025-05-07T15:35:10.037Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimItemValidationStep/page.mdx": "2025-04-11T09:04:37.482Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimItemWorkflow/page.mdx": "2025-05-01T15:18:37.777Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimItemWorkflow/page.mdx": "2025-05-07T15:35:10.062Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimRequestItemReturnValidationStep/page.mdx": "2025-04-11T09:04:37.537Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimRequestItemReturnWorkflow/page.mdx": "2025-05-01T15:18:37.805Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderClaimRequestItemReturnWorkflow/page.mdx": "2025-05-07T15:35:10.090Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditAddNewItemValidationStep/page.mdx": "2025-04-11T09:04:38.900Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditAddNewItemWorkflow/page.mdx": "2025-05-01T15:18:38.530Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditAddNewItemWorkflow/page.mdx": "2025-05-07T15:35:10.805Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditUpdateItemQuantityValidationStep/page.mdx": "2025-04-11T09:04:38.995Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditUpdateItemQuantityWorkflow/page.mdx": "2025-05-01T15:18:38.553Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderExchangeAddNewItemWorkflow/page.mdx": "2025-05-01T15:18:38.259Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderExchangeRequestItemReturnWorkflow/page.mdx": "2025-05-01T15:18:38.288Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderEditUpdateItemQuantityWorkflow/page.mdx": "2025-05-07T15:35:10.829Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderExchangeAddNewItemWorkflow/page.mdx": "2025-05-07T15:35:10.544Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.orderExchangeRequestItemReturnWorkflow/page.mdx": "2025-05-07T15:35:10.571Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveItemReturnRequestValidationStep/page.mdx": "2025-04-11T09:04:39.683Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveItemReturnRequestWorkflow/page.mdx": "2025-05-01T15:18:38.906Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeAddItemClaimActionWorkflow/page.mdx": "2025-05-01T15:18:37.880Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveItemReturnRequestWorkflow/page.mdx": "2025-05-07T15:35:11.176Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeAddItemClaimActionWorkflow/page.mdx": "2025-05-07T15:35:10.167Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeClaimAddItemActionValidationStep/page.mdx": "2025-04-11T09:04:37.694Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeClaimItemActionValidationStep/page.mdx": "2025-04-11T09:04:37.744Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeClaimShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:37.784Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeClaimShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:37.925Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeClaimShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:10.212Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeExchangeItemActionValidationStep/page.mdx": "2025-04-11T09:04:38.489Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeExchangeShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:38.530Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeExchangeShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.340Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemClaimActionWorkflow/page.mdx": "2025-05-01T15:18:37.905Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemExchangeActionWorkflow/page.mdx": "2025-05-01T15:18:38.316Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemOrderEditActionWorkflow/page.mdx": "2025-05-01T15:18:38.576Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeExchangeShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:10.618Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemClaimActionWorkflow/page.mdx": "2025-05-07T15:35:10.191Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemExchangeActionWorkflow/page.mdx": "2025-05-07T15:35:10.597Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemOrderEditActionWorkflow/page.mdx": "2025-05-07T15:35:10.853Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemReceiveReturnActionValidationStep/page.mdx": "2025-04-11T09:04:39.734Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemReceiveReturnActionWorkflow/page.mdx": "2025-05-01T15:18:38.932Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemReturnActionWorkflow/page.mdx": "2025-05-01T15:18:38.957Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemReceiveReturnActionWorkflow/page.mdx": "2025-05-07T15:35:11.202Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeItemReturnActionWorkflow/page.mdx": "2025-05-07T15:35:11.228Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeOrderEditItemActionValidationStep/page.mdx": "2025-04-11T09:04:39.042Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeOrderEditShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:39.082Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeOrderEditShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.595Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeOrderEditShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:10.871Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeReturnItemActionValidationStep/page.mdx": "2025-04-11T09:04:39.785Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeReturnShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:39.825Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeReturnShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.979Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.removeReturnShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:11.249Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestItemReturnValidationStep/page.mdx": "2025-04-11T09:04:39.866Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestItemReturnWorkflow/page.mdx": "2025-05-01T15:18:39.000Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestItemReturnWorkflow/page.mdx": "2025-05-07T15:35:11.270Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestOrderEditRequestValidationStep/page.mdx": "2025-04-11T09:04:39.127Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestOrderEditRequestWorkflow/page.mdx": "2025-05-01T15:18:38.617Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimAddItemValidationStep/page.mdx": "2025-04-11T09:04:37.834Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimAddItemWorkflow/page.mdx": "2025-05-01T15:18:37.950Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimAddItemWorkflow/page.mdx": "2025-05-07T15:35:10.236Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimItemValidationStep/page.mdx": "2025-04-11T09:04:37.917Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimItemWorkflow/page.mdx": "2025-05-01T15:18:37.974Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimItemWorkflow/page.mdx": "2025-05-07T15:35:10.262Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:37.960Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateClaimShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:37.996Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateExchangeAddItemValidationStep/page.mdx": "2025-04-11T09:04:38.578Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateExchangeAddItemWorkflow/page.mdx": "2025-05-01T15:18:38.366Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateExchangeAddItemWorkflow/page.mdx": "2025-05-07T15:35:10.644Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateExchangeShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:38.619Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateExchangeShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.391Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderChangesWorkflow/page.mdx": "2025-05-01T15:18:39.182Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderChangesWorkflow/page.mdx": "2025-05-07T15:35:11.453Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditAddItemValidationStep/page.mdx": "2025-04-11T09:04:39.176Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditAddItemWorkflow/page.mdx": "2025-05-01T15:18:38.641Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditAddItemWorkflow/page.mdx": "2025-05-07T15:35:10.918Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditItemQuantityValidationStep/page.mdx": "2025-04-11T09:04:39.223Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditItemQuantityWorkflow/page.mdx": "2025-05-01T15:18:38.663Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditItemQuantityWorkflow/page.mdx": "2025-05-07T15:35:10.942Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:39.260Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderEditShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:38.683Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReceiveItemReturnRequestValidationStep/page.mdx": "2025-04-11T09:04:39.918Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReceiveItemReturnRequestWorkflow/page.mdx": "2025-05-01T15:18:39.026Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReceiveItemReturnRequestWorkflow/page.mdx": "2025-05-07T15:35:11.296Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateRequestItemReturnValidationStep/page.mdx": "2025-04-11T09:04:39.969Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateRequestItemReturnWorkflow/page.mdx": "2025-05-01T15:18:39.053Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateRequestItemReturnWorkflow/page.mdx": "2025-05-07T15:35:11.322Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReturnShippingMethodValidationStep/page.mdx": "2025-04-11T09:04:40.083Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReturnShippingMethodWorkflow/page.mdx": "2025-05-01T15:18:39.095Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReturnValidationStep/page.mdx": "2025-04-11T09:04:40.037Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReturnWorkflow/page.mdx": "2025-05-01T15:18:39.073Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateReturnWorkflow/page.mdx": "2025-05-07T15:35:11.342Z", "references/core_flows/Order/core_flows.Order.Steps_Order/page.mdx": "2025-04-23T16:21:19.028Z", "references/core_flows/Order/core_flows.Order.Workflows_Order/page.mdx": "2025-04-23T16:21:19.362Z", "references/core_flows/Shipping_Options/Steps_Shipping_Options/functions/core_flows.Shipping_Options.Steps_Shipping_Options.listShippingOptionsForContextStep/page.mdx": "2025-04-11T09:04:41.407Z", @@ -632,38 +632,38 @@ export const generatedEditDates = { "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.updateCartsStep/page.mdx": "2025-04-11T09:04:35.629Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateCartPaymentsStep/page.mdx": "2025-04-11T09:04:35.679Z", "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.addToCartWorkflow/page.mdx": "2025-05-01T15:18:36.619Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.confirmVariantInventoryWorkflow/page.mdx": "2025-05-01T15:18:36.627Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.confirmVariantInventoryWorkflow/page.mdx": "2025-05-07T15:35:08.763Z", "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.createCartWorkflow/page.mdx": "2025-05-01T15:18:36.648Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.createPaymentCollectionForCartWorkflow/page.mdx": "2025-05-01T15:18:36.658Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWorkflow/page.mdx": "2025-05-01T15:18:36.668Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.createPaymentCollectionForCartWorkflow/page.mdx": "2025-05-07T15:35:08.840Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWorkflow/page.mdx": "2025-05-07T15:35:08.850Z", "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateCartWorkflow/page.mdx": "2025-05-01T15:18:36.684Z", "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateLineItemInCartWorkflow/page.mdx": "2025-05-01T15:18:36.691Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateTaxLinesWorkflow/page.mdx": "2025-05-01T15:18:36.698Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateTaxLinesWorkflow/page.mdx": "2025-05-07T15:35:08.885Z", "references/core_flows/Cart/core_flows.Cart.Steps_Cart/page.mdx": "2025-02-11T11:36:38.987Z", "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.useRemoteQueryStep/page.mdx": "2025-01-13T17:30:23.158Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createShippingOptionRulesStep/page.mdx": "2025-05-01T14:14:04.304Z", - "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.deleteShippingOptionRulesStep/page.mdx": "2025-05-01T15:18:37.166Z", + "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.deleteShippingOptionRulesStep/page.mdx": "2025-05-07T15:35:09.465Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderClaimStep/page.mdx": "2025-01-17T16:43:22.988Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderExchangeStep/page.mdx": "2025-01-17T16:43:23.041Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderReturnStep/page.mdx": "2025-01-17T16:43:23.074Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrdersStep/page.mdx": "2025-04-11T09:04:36.797Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderTaxLinesWorkflow/page.mdx": "2025-05-01T15:18:39.208Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderTaxLinesWorkflow/page.mdx": "2025-05-07T15:35:11.477Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.createPriceListPricesStep/page.mdx": "2025-04-11T09:04:40.588Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.createPriceListsStep/page.mdx": "2025-04-11T09:04:40.594Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.updatePriceListPricesStep/page.mdx": "2025-04-11T09:04:40.611Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.batchPriceListPricesWorkflow/page.mdx": "2025-05-01T15:18:39.403Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.createPriceListPricesWorkflow/page.mdx": "2025-05-01T15:18:39.407Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.createPriceListsWorkflow/page.mdx": "2025-05-01T15:18:39.412Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.updatePriceListPricesWorkflow/page.mdx": "2025-05-01T15:18:39.419Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.updatePriceListsWorkflow/page.mdx": "2025-05-01T15:18:39.422Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.batchPriceListPricesWorkflow/page.mdx": "2025-05-07T15:35:11.611Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.createPriceListPricesWorkflow/page.mdx": "2025-05-07T15:35:11.616Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.createPriceListsWorkflow/page.mdx": "2025-05-07T15:35:11.620Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.updatePriceListPricesWorkflow/page.mdx": "2025-05-07T15:35:11.627Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.updatePriceListsWorkflow/page.mdx": "2025-05-07T15:35:11.629Z", "references/core_flows/Price_List/core_flows.Price_List.Steps_Price_List/page.mdx": "2024-12-23T12:30:26.076Z", "references/core_flows/Price_List/core_flows.Price_List.Workflows_Price_List/page.mdx": "2024-12-23T12:30:26.103Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.addRulesToPromotionsStep/page.mdx": "2025-04-11T09:04:41.123Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.removeRulesFromPromotionsStep/page.mdx": "2025-01-13T18:05:52.283Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.updatePromotionRulesStep/page.mdx": "2025-04-11T09:04:41.151Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.createPromotionRulesWorkflow/page.mdx": "2025-05-01T15:18:39.754Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.deletePromotionRulesWorkflow/page.mdx": "2025-05-01T15:18:39.762Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionRulesWorkflow/page.mdx": "2025-05-01T15:18:39.769Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.createPromotionRulesWorkflow/page.mdx": "2025-05-07T15:35:11.884Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.deletePromotionRulesWorkflow/page.mdx": "2025-05-07T15:35:11.892Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionRulesWorkflow/page.mdx": "2025-05-07T15:35:11.899Z", "references/core_flows/Promotion/core_flows.Promotion.Steps_Promotion/page.mdx": "2024-12-23T12:30:26.321Z", "references/core_flows/Promotion/core_flows.Promotion.Workflows_Promotion/page.mdx": "2024-12-23T12:30:26.343Z", "references/core_flows/core_flows.Steps_Cart/page.mdx": "2024-12-09T13:21:37.605Z", @@ -718,10 +718,10 @@ export const generatedEditDates = { "references/cart/interfaces/cart.LineItemAdjustmentDTO/page.mdx": "2025-04-11T09:04:43.840Z", "references/cart/interfaces/cart.LineItemTaxLineDTO/page.mdx": "2025-04-11T09:04:43.862Z", "references/cart/interfaces/cart.UpdateLineItemWithSelectorDTO/page.mdx": "2025-04-11T09:04:43.987Z", - "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.createCartsStep/page.mdx": "2025-05-01T15:18:36.473Z", + "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.createCartsStep/page.mdx": "2025-05-07T15:35:08.595Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.createLineItemsStep/page.mdx": "2025-04-11T09:04:35.478Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.getActionsToComputeFromPromotionsStep/page.mdx": "2025-04-11T09:04:35.520Z", - "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.retrieveCartStep/page.mdx": "2025-05-01T15:18:36.543Z", + "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.retrieveCartStep/page.mdx": "2025-05-07T15:35:08.668Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.updateLineItemsStep/page.mdx": "2025-04-11T09:04:35.657Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateCartShippingOptionsStep/page.mdx": "2025-04-11T09:04:35.692Z", "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.validateExistingPaymentCollectionStep/page.mdx": "2025-04-11T09:04:35.832Z", @@ -789,7 +789,7 @@ export const generatedEditDates = { "references/types/HttpTypes/interfaces/types.HttpTypes.AdminTaxRegionDeleteResponse/page.mdx": "2024-12-09T13:21:34.725Z", "references/types/HttpTypes/types/types.HttpTypes.DeleteResponse/page.mdx": "2024-12-09T13:21:33.545Z", "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.validateRefundStep/page.mdx": "2025-04-11T09:04:40.478Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.addOrderTransactionStep/page.mdx": "2025-05-01T15:18:37.386Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.addOrderTransactionStep/page.mdx": "2025-05-07T15:35:09.683Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.archiveOrdersStep/page.mdx": "2025-04-11T09:04:36.761Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.completeOrdersStep/page.mdx": "2025-04-11T09:04:36.903Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrdersStep/page.mdx": "2025-04-11T09:04:36.982Z", @@ -804,11 +804,11 @@ export const generatedEditDates = { "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelValidateOrder/page.mdx": "2025-04-11T09:04:37.336Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.completeOrderWorkflow/page.mdx": "2025-05-01T15:18:38.012Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createFulfillmentValidateOrder/page.mdx": "2025-04-11T09:04:38.038Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createShipmentValidateOrder/page.mdx": "2025-04-11T09:04:38.168Z", - "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.refundPaymentWorkflow/page.mdx": "2025-05-01T15:18:39.322Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createShipmentValidateOrder/page.mdx": "2025-05-07T15:35:10.414Z", + "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.refundPaymentWorkflow/page.mdx": "2025-05-07T15:35:11.535Z", "references/core_flows/Payment/core_flows.Payment.Workflows_Payment/page.mdx": "2025-01-07T12:54:17.428Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.updatePromotionsStep/page.mdx": "2025-04-11T09:04:41.156Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionsWorkflow/page.mdx": "2025-05-01T15:18:39.772Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionsWorkflow/page.mdx": "2025-05-07T15:35:11.903Z", "references/core_flows/core_flows.Workflows_Payment/page.mdx": "2024-12-09T13:21:48.376Z", "references/core_flows/interfaces/core_flows.SetOrderTaxLinesForItemsStepInput/page.mdx": "2025-04-11T09:04:42.017Z", "references/fulfillment/interfaces/fulfillment.OrderSummaryDTO/page.mdx": "2025-02-24T10:48:36.235Z", @@ -842,7 +842,7 @@ export const generatedEditDates = { "references/auth/IAuthModuleService/methods/auth.IAuthModuleService.authenticate/page.mdx": "2025-01-07T12:54:18.941Z", "references/auth/IAuthModuleService/methods/auth.IAuthModuleService.validateCallback/page.mdx": "2025-01-07T12:54:18.948Z", "references/auth/interfaces/auth.AuthenticationResponse/page.mdx": "2024-12-09T13:21:36.233Z", - "references/auth_provider/classes/auth_provider.AbstractAuthModuleProvider/page.mdx": "2025-05-01T15:18:45.919Z", + "references/auth_provider/classes/auth_provider.AbstractAuthModuleProvider/page.mdx": "2025-05-07T15:35:18.127Z", "references/core_flows/Invite/Workflows_Invite/functions/core_flows.Invite.Workflows_Invite.refreshInviteTokensWorkflow/page.mdx": "2025-05-01T15:18:37.347Z", "references/types/CommonTypes/types/types.CommonTypes.BatchMethodResponse/page.mdx": "2024-12-09T13:21:32.849Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminAddReturnShipping/page.mdx": "2025-04-11T09:04:47.540Z", @@ -929,9 +929,9 @@ export const generatedEditDates = { "references/core_flows/Auth/core_flows.Auth.Steps_Auth/page.mdx": "2024-12-23T12:30:23.503Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderClaimItemsFromActionsStep/page.mdx": "2025-04-11T09:04:36.809Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderExchangeItemsFromActionsStep/page.mdx": "2025-04-11T09:04:37.037Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderChangeActionsStep/page.mdx": "2025-05-01T15:18:37.600Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderChangeActionsWorkflow/page.mdx": "2025-05-01T15:18:38.044Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderChangeActionsWorkflow/page.mdx": "2025-05-01T15:18:39.168Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderChangeActionsStep/page.mdx": "2025-05-07T15:35:09.892Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderChangeActionsWorkflow/page.mdx": "2025-05-07T15:35:10.335Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.updateOrderChangeActionsWorkflow/page.mdx": "2025-05-07T15:35:11.439Z", "references/core_flows/core_flows.Auth/page.mdx": "2024-12-23T12:30:23.503Z", "references/core_flows/core_flows.Steps_Auth/page.mdx": "2024-12-09T13:21:37.257Z", "references/core_flows/types/core_flows.CreateOrderClaimItemsFromActionsInput/page.mdx": "2025-04-11T09:04:41.976Z", @@ -954,7 +954,7 @@ export const generatedEditDates = { "references/api_key/types/api_key.ReadonlyPrimary/page.mdx": "2024-09-17T00:10:59.567Z", "references/api_key/types/api_key.Scalar/page.mdx": "2024-09-17T00:10:59.567Z", "references/core_flows/Customer/Workflows_Customer/functions/core_flows.Customer.Workflows_Customer.removeCustomerAccountWorkflow/page.mdx": "2025-05-01T15:18:36.766Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.markFulfillmentAsDeliveredWorkflow/page.mdx": "2025-05-01T15:18:37.263Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.markFulfillmentAsDeliveredWorkflow/page.mdx": "2025-05-07T15:35:09.560Z", "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.validateFulfillmentDeliverabilityStep/page.mdx": "2025-01-20T08:25:20.588Z", "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.validateInventoryLevelsDelete/page.mdx": "2025-01-20T08:25:20.641Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.markOrderFulfillmentAsDeliveredWorkflow/page.mdx": "2025-05-01T15:18:38.424Z", @@ -1055,8 +1055,8 @@ export const generatedEditDates = { "references/cart/types/cart.JoinerRelationship/page.mdx": "2024-12-09T13:21:36.889Z", "references/core_flows/Api_Key/Steps_Api_Key/functions/core_flows.Api_Key.Steps_Api_Key.revokeApiKeysStep/page.mdx": "2025-04-11T09:04:35.334Z", "references/core_flows/Api_Key/Steps_Api_Key/functions/core_flows.Api_Key.Steps_Api_Key.updateApiKeysStep/page.mdx": "2025-04-11T09:04:35.339Z", - "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.revokeApiKeysWorkflow/page.mdx": "2025-05-01T15:18:36.438Z", - "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.updateApiKeysWorkflow/page.mdx": "2025-05-01T15:18:36.443Z", + "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.revokeApiKeysWorkflow/page.mdx": "2025-05-07T15:35:08.560Z", + "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.updateApiKeysWorkflow/page.mdx": "2025-05-07T15:35:08.565Z", "references/core_flows/Api_Key/core_flows.Api_Key.Steps_Api_Key/page.mdx": "2024-12-23T12:30:23.467Z", "references/core_flows/Api_Key/core_flows.Api_Key.Workflows_Api_Key/page.mdx": "2024-12-23T12:30:23.484Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.findOrCreateCustomerStep/page.mdx": "2025-01-13T17:30:22.980Z", @@ -1064,12 +1064,12 @@ export const generatedEditDates = { "references/core_flows/Customer/Workflows_Customer/functions/core_flows.Customer.Workflows_Customer.createCustomerAddressesWorkflow/page.mdx": "2025-05-01T15:18:36.753Z", "references/core_flows/Customer/Workflows_Customer/functions/core_flows.Customer.Workflows_Customer.deleteCustomerAddressesWorkflow/page.mdx": "2025-05-01T15:18:36.762Z", "references/core_flows/Customer/Workflows_Customer/functions/core_flows.Customer.Workflows_Customer.updateCustomerAddressesWorkflow/page.mdx": "2025-05-01T15:18:36.774Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.bulkCreateDeleteLevelsWorkflow/page.mdx": "2025-05-01T15:18:37.310Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.deleteInventoryLevelsWorkflow/page.mdx": "2025-05-01T15:18:37.323Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.bulkCreateDeleteLevelsWorkflow/page.mdx": "2025-05-07T15:35:09.606Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.deleteInventoryLevelsWorkflow/page.mdx": "2025-05-07T15:35:09.619Z", "references/core_flows/Inventory/core_flows.Inventory.Steps_Inventory/page.mdx": "2024-12-23T12:30:23.988Z", "references/core_flows/Inventory/core_flows.Inventory.Workflows_Inventory/page.mdx": "2025-01-13T17:30:23.526Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.createOrderLineItemsStep/page.mdx": "2025-04-23T16:21:19.201Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateReturnsStep/page.mdx": "2025-05-01T15:18:37.585Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateReturnsStep/page.mdx": "2025-05-07T15:35:09.876Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.addOrderLineItemsWorkflow/page.mdx": "2025-05-01T15:18:37.648Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderWorkflow/page.mdx": "2025-05-01T15:18:37.684Z", "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.createCollectionsWorkflow/page.mdx": "2025-05-01T15:18:39.550Z", @@ -1097,11 +1097,11 @@ export const generatedEditDates = { "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.createTaxRateRulesStep/page.mdx": "2025-04-11T09:04:41.501Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.createTaxRatesStep/page.mdx": "2025-04-11T09:04:41.507Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.updateTaxRatesStep/page.mdx": "2025-04-11T09:04:41.547Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRateRulesWorkflow/page.mdx": "2025-05-01T15:18:39.957Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRatesWorkflow/page.mdx": "2025-05-01T15:18:39.959Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRateRulesWorkflow/page.mdx": "2025-05-07T15:35:12.084Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRatesWorkflow/page.mdx": "2025-05-07T15:35:12.086Z", "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.maybeListTaxRateRuleIdsStep/page.mdx": "2025-01-20T08:25:22.551Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.setTaxRateRulesWorkflow/page.mdx": "2025-05-01T15:18:39.970Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.updateTaxRatesWorkflow/page.mdx": "2025-05-01T15:18:39.976Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.setTaxRateRulesWorkflow/page.mdx": "2025-05-07T15:35:12.098Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.updateTaxRatesWorkflow/page.mdx": "2025-05-07T15:35:12.105Z", "references/core_flows/Tax/core_flows.Tax.Steps_Tax/page.mdx": "2025-01-07T12:54:18.172Z", "references/core_flows/Tax/core_flows.Tax.Workflows_Tax/page.mdx": "2024-12-23T12:30:26.543Z", "references/core_flows/core_flows.Steps_Api_Key/page.mdx": "2024-12-09T13:21:37.221Z", @@ -1701,9 +1701,9 @@ export const generatedEditDates = { "references/core_flows/Api_Key/Steps_Api_Key/functions/core_flows.Api_Key.Steps_Api_Key.deleteApiKeysStep/page.mdx": "2025-01-13T17:30:22.900Z", "references/core_flows/Api_Key/Steps_Api_Key/functions/core_flows.Api_Key.Steps_Api_Key.linkSalesChannelsToApiKeyStep/page.mdx": "2025-01-13T18:05:49.177Z", "references/core_flows/Api_Key/Steps_Api_Key/functions/core_flows.Api_Key.Steps_Api_Key.validateSalesChannelsExistStep/page.mdx": "2025-01-13T17:30:22.912Z", - "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.createApiKeysWorkflow/page.mdx": "2025-05-01T15:18:36.426Z", - "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.deleteApiKeysWorkflow/page.mdx": "2025-05-01T15:18:36.430Z", - "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.linkSalesChannelsToApiKeyWorkflow/page.mdx": "2025-05-01T15:18:36.433Z", + "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.createApiKeysWorkflow/page.mdx": "2025-05-07T15:35:08.546Z", + "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.deleteApiKeysWorkflow/page.mdx": "2025-05-07T15:35:08.552Z", + "references/core_flows/Api_Key/Workflows_Api_Key/functions/core_flows.Api_Key.Workflows_Api_Key.linkSalesChannelsToApiKeyWorkflow/page.mdx": "2025-05-07T15:35:08.556Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.addShippingMethodToCartStep/page.mdx": "2025-04-11T09:04:35.417Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.confirmInventoryStep/page.mdx": "2025-03-04T13:33:40.224Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.createLineItemAdjustmentsStep/page.mdx": "2025-03-04T13:33:40.246Z", @@ -1719,10 +1719,10 @@ export const generatedEditDates = { "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.removeShippingMethodAdjustmentsStep/page.mdx": "2025-01-13T17:30:23.012Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.removeShippingMethodFromCartStep/page.mdx": "2025-01-13T17:30:23.013Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.reserveInventoryStep/page.mdx": "2025-04-11T09:04:35.568Z", - "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.updateCartPromotionsStep/page.mdx": "2025-05-01T15:18:36.548Z", + "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.updateCartPromotionsStep/page.mdx": "2025-05-07T15:35:08.674Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateVariantPricesStep/page.mdx": "2025-01-13T18:05:49.333Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshPaymentCollectionForCartWorkflow/page.mdx": "2025-05-01T15:18:36.677Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateCartPromotionsWorkflow/page.mdx": "2025-05-01T15:18:36.687Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshPaymentCollectionForCartWorkflow/page.mdx": "2025-05-07T15:35:08.862Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.updateCartPromotionsWorkflow/page.mdx": "2025-05-07T15:35:08.873Z", "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.createCustomerAddressesStep/page.mdx": "2025-04-11T09:04:35.986Z", "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.deleteCustomerAddressesStep/page.mdx": "2025-01-13T17:30:23.181Z", "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.deleteCustomersStep/page.mdx": "2025-01-13T17:30:23.183Z", @@ -1730,134 +1730,134 @@ export const generatedEditDates = { "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.maybeUnsetDefaultShippingAddressesStep/page.mdx": "2025-01-13T17:30:23.186Z", "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.updateCustomerAddressesStep/page.mdx": "2025-04-11T09:04:36.011Z", "references/core_flows/Customer/Steps_Customer/functions/core_flows.Customer.Steps_Customer.updateCustomersStep/page.mdx": "2025-04-11T09:04:36.018Z", - "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.createCustomerGroupsStep/page.mdx": "2025-05-01T15:18:36.783Z", + "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.createCustomerGroupsStep/page.mdx": "2025-05-07T15:35:08.977Z", "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.deleteCustomerGroupStep/page.mdx": "2025-01-13T17:30:23.255Z", "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.linkCustomersToCustomerGroupStep/page.mdx": "2025-01-13T18:05:49.505Z", "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.updateCustomerGroupsStep/page.mdx": "2025-04-11T09:04:36.117Z", - "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.createCustomerGroupsWorkflow/page.mdx": "2025-05-01T15:18:36.801Z", - "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.deleteCustomerGroupsWorkflow/page.mdx": "2025-05-01T15:18:36.798Z", - "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.linkCustomersToCustomerGroupWorkflow/page.mdx": "2025-05-01T15:18:36.805Z", - "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.updateCustomerGroupsWorkflow/page.mdx": "2025-05-01T15:18:36.796Z", + "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.createCustomerGroupsWorkflow/page.mdx": "2025-05-07T15:35:08.994Z", + "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.deleteCustomerGroupsWorkflow/page.mdx": "2025-05-07T15:35:08.992Z", + "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.linkCustomersToCustomerGroupWorkflow/page.mdx": "2025-05-07T15:35:08.999Z", + "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.updateCustomerGroupsWorkflow/page.mdx": "2025-05-07T15:35:08.990Z", "references/core_flows/Defaults/Steps_Defaults/functions/core_flows.Defaults.Steps_Defaults.createDefaultStoreStep/page.mdx": "2025-01-13T17:30:23.305Z", - "references/core_flows/Defaults/Workflows_Defaults/functions/core_flows.Defaults.Workflows_Defaults.createDefaultsWorkflow/page.mdx": "2025-05-01T15:18:36.811Z", + "references/core_flows/Defaults/Workflows_Defaults/functions/core_flows.Defaults.Workflows_Defaults.createDefaultsWorkflow/page.mdx": "2025-05-07T15:35:09.006Z", "references/core_flows/File/Steps_File/functions/core_flows.File.Steps_File.deleteFilesStep/page.mdx": "2025-04-23T16:21:18.753Z", "references/core_flows/File/Steps_File/functions/core_flows.File.Steps_File.uploadFilesStep/page.mdx": "2025-04-23T16:21:18.755Z", - "references/core_flows/File/Workflows_File/functions/core_flows.File.Workflows_File.deleteFilesWorkflow/page.mdx": "2025-05-01T15:18:37.114Z", - "references/core_flows/File/Workflows_File/functions/core_flows.File.Workflows_File.uploadFilesWorkflow/page.mdx": "2025-05-01T15:18:37.112Z", + "references/core_flows/File/Workflows_File/functions/core_flows.File.Workflows_File.deleteFilesWorkflow/page.mdx": "2025-05-07T15:35:09.408Z", + "references/core_flows/File/Workflows_File/functions/core_flows.File.Workflows_File.uploadFilesWorkflow/page.mdx": "2025-05-07T15:35:09.406Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.cancelFulfillmentStep/page.mdx": "2025-01-13T17:30:23.344Z", - "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createFulfillmentSets/page.mdx": "2025-05-01T15:18:37.125Z", - "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createServiceZonesStep/page.mdx": "2025-05-01T15:18:37.153Z", - "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createShippingOptionsPriceSetsStep/page.mdx": "2025-05-01T15:18:37.120Z", - "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createShippingProfilesStep/page.mdx": "2025-05-01T15:18:37.161Z", + "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createFulfillmentSets/page.mdx": "2025-05-07T15:35:09.421Z", + "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createServiceZonesStep/page.mdx": "2025-05-07T15:35:09.452Z", + "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createShippingOptionsPriceSetsStep/page.mdx": "2025-05-07T15:35:09.414Z", + "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.createShippingProfilesStep/page.mdx": "2025-05-07T15:35:09.460Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.deleteFulfillmentSetsStep/page.mdx": "2025-01-13T17:30:23.386Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.deleteServiceZonesStep/page.mdx": "2025-01-13T17:30:23.387Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.deleteShippingOptionsStep/page.mdx": "2025-01-13T17:30:23.392Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.setShippingOptionsPricesStep/page.mdx": "2025-01-13T18:05:49.596Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.updateShippingProfilesStep/page.mdx": "2025-04-11T09:04:36.323Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.validateShipmentStep/page.mdx": "2025-01-13T17:30:23.408Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.batchShippingOptionRulesWorkflow/page.mdx": "2025-05-01T15:18:37.202Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.cancelFulfillmentWorkflow/page.mdx": "2025-05-01T15:18:37.207Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createServiceZonesWorkflow/page.mdx": "2025-05-01T15:18:37.227Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShippingOptionsWorkflow/page.mdx": "2025-05-01T15:18:37.240Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShippingProfilesWorkflow/page.mdx": "2025-05-01T15:18:37.245Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteFulfillmentSetsWorkflow/page.mdx": "2025-05-01T15:18:37.247Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteServiceZonesWorkflow/page.mdx": "2025-05-01T15:18:37.249Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteShippingOptionsWorkflow/page.mdx": "2025-05-01T15:18:37.251Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateServiceZonesWorkflow/page.mdx": "2025-05-01T15:18:37.276Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateShippingOptionsWorkflow/page.mdx": "2025-05-01T15:18:37.278Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateShippingProfilesWorkflow/page.mdx": "2025-05-01T15:18:37.282Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.batchShippingOptionRulesWorkflow/page.mdx": "2025-05-07T15:35:09.501Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.cancelFulfillmentWorkflow/page.mdx": "2025-05-07T15:35:09.505Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createServiceZonesWorkflow/page.mdx": "2025-05-07T15:35:09.523Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShippingOptionsWorkflow/page.mdx": "2025-05-07T15:35:09.537Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.createShippingProfilesWorkflow/page.mdx": "2025-05-07T15:35:09.541Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteFulfillmentSetsWorkflow/page.mdx": "2025-05-07T15:35:09.544Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteServiceZonesWorkflow/page.mdx": "2025-05-07T15:35:09.546Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.deleteShippingOptionsWorkflow/page.mdx": "2025-05-07T15:35:09.548Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateServiceZonesWorkflow/page.mdx": "2025-05-07T15:35:09.572Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateShippingOptionsWorkflow/page.mdx": "2025-05-07T15:35:09.574Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.updateShippingProfilesWorkflow/page.mdx": "2025-05-07T15:35:09.579Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.adjustInventoryLevelsStep/page.mdx": "2025-04-11T09:04:36.525Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.attachInventoryItemToVariants/page.mdx": "2025-01-13T17:30:23.510Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.createInventoryItemsStep/page.mdx": "2025-04-11T09:04:36.533Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.createInventoryLevelsStep/page.mdx": "2025-04-11T09:04:36.537Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.deleteInventoryItemStep/page.mdx": "2025-01-13T17:30:23.517Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.deleteInventoryLevelsStep/page.mdx": "2025-01-13T17:30:23.519Z", - "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.updateInventoryItemsStep/page.mdx": "2025-05-01T15:18:37.298Z", + "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.updateInventoryItemsStep/page.mdx": "2025-05-07T15:35:09.595Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.updateInventoryLevelsStep/page.mdx": "2025-04-11T09:04:36.558Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.validateInventoryItemsForCreate/page.mdx": "2025-01-13T17:30:23.525Z", "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.validateInventoryLocationsStep/page.mdx": "2025-01-13T17:30:23.524Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.createInventoryItemsWorkflow/page.mdx": "2025-05-01T15:18:37.313Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.createInventoryLevelsWorkflow/page.mdx": "2025-05-01T15:18:37.315Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.deleteInventoryItemWorkflow/page.mdx": "2025-05-01T15:18:37.317Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.updateInventoryItemsWorkflow/page.mdx": "2025-05-01T15:18:37.325Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.updateInventoryLevelsWorkflow/page.mdx": "2025-05-01T15:18:37.328Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.createInventoryItemsWorkflow/page.mdx": "2025-05-07T15:35:09.610Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.createInventoryLevelsWorkflow/page.mdx": "2025-05-07T15:35:09.612Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.deleteInventoryItemWorkflow/page.mdx": "2025-05-07T15:35:09.614Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.updateInventoryItemsWorkflow/page.mdx": "2025-05-07T15:35:09.622Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.updateInventoryLevelsWorkflow/page.mdx": "2025-05-07T15:35:09.625Z", "references/core_flows/Invite/Steps_Invite/functions/core_flows.Invite.Steps_Invite.createInviteStep/page.mdx": "2025-04-11T09:04:36.620Z", "references/core_flows/Invite/Steps_Invite/functions/core_flows.Invite.Steps_Invite.deleteInvitesStep/page.mdx": "2025-01-13T17:30:23.561Z", "references/core_flows/Invite/Steps_Invite/functions/core_flows.Invite.Steps_Invite.refreshInviteTokensStep/page.mdx": "2025-04-11T09:04:36.627Z", "references/core_flows/Invite/Steps_Invite/functions/core_flows.Invite.Steps_Invite.validateTokenStep/page.mdx": "2025-01-13T17:30:23.565Z", "references/core_flows/Line_Item/Steps_Line_Item/functions/core_flows.Line_Item.Steps_Line_Item.deleteLineItemsStep/page.mdx": "2025-01-13T17:30:23.577Z", - "references/core_flows/Line_Item/Workflows_Line_Item/functions/core_flows.Line_Item.Workflows_Line_Item.deleteLineItemsWorkflow/page.mdx": "2025-05-01T15:18:37.377Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderChangeStep/page.mdx": "2025-05-01T15:18:37.404Z", + "references/core_flows/Line_Item/Workflows_Line_Item/functions/core_flows.Line_Item.Workflows_Line_Item.deleteLineItemsWorkflow/page.mdx": "2025-05-07T15:35:09.673Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderChangeStep/page.mdx": "2025-05-07T15:35:09.705Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.cancelOrderFulfillmentStep/page.mdx": "2025-01-13T18:05:49.821Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.declineOrderChangeStep/page.mdx": "2025-05-01T15:18:37.492Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteClaimsStep/page.mdx": "2025-05-01T15:18:37.440Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteExchangesStep/page.mdx": "2025-05-01T15:18:37.517Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderChangeActionsStep/page.mdx": "2025-05-01T15:18:37.494Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderChangesStep/page.mdx": "2025-05-01T15:18:37.496Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderLineItems/page.mdx": "2025-05-01T15:18:37.493Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderShippingMethods/page.mdx": "2025-05-01T15:18:37.496Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteReturnsStep/page.mdx": "2025-05-01T15:18:37.581Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.declineOrderChangeStep/page.mdx": "2025-05-07T15:35:09.794Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteClaimsStep/page.mdx": "2025-05-07T15:35:09.740Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteExchangesStep/page.mdx": "2025-05-07T15:35:09.819Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderChangeActionsStep/page.mdx": "2025-05-07T15:35:09.797Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderChangesStep/page.mdx": "2025-05-07T15:35:09.798Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderLineItems/page.mdx": "2025-05-07T15:35:09.795Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteOrderShippingMethods/page.mdx": "2025-05-07T15:35:09.799Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.deleteReturnsStep/page.mdx": "2025-05-07T15:35:09.873Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.registerOrderFulfillmentStep/page.mdx": "2025-01-13T18:05:49.913Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.registerOrderShipmentStep/page.mdx": "2025-01-13T17:30:23.699Z", - "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderShippingMethodsStep/page.mdx": "2025-05-01T15:18:37.638Z", + "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrderShippingMethodsStep/page.mdx": "2025-05-07T15:35:09.928Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateReturnItemsStep/page.mdx": "2025-01-13T18:05:49.942Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderClaimWorkflow/page.mdx": "2025-05-01T15:18:37.719Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderClaimWorkflow/page.mdx": "2025-05-07T15:35:10.005Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderEditWorkflow/page.mdx": "2025-05-01T15:18:38.458Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderExchangeWorkflow/page.mdx": "2025-05-01T15:18:38.173Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderChangeWorkflow/page.mdx": "2025-05-01T15:18:37.666Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderClaimWorkflow/page.mdx": "2025-05-01T15:18:37.727Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderExchangeWorkflow/page.mdx": "2025-05-01T15:18:38.182Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnReceiveWorkflow/page.mdx": "2025-05-01T15:18:38.738Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnRequestWorkflow/page.mdx": "2025-05-01T15:18:38.752Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnWorkflow/page.mdx": "2025-05-01T15:18:38.757Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelBeginOrderExchangeWorkflow/page.mdx": "2025-05-07T15:35:10.460Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderChangeWorkflow/page.mdx": "2025-05-07T15:35:09.956Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderClaimWorkflow/page.mdx": "2025-05-07T15:35:10.012Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderExchangeWorkflow/page.mdx": "2025-05-07T15:35:10.467Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnReceiveWorkflow/page.mdx": "2025-05-07T15:35:11.017Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnRequestWorkflow/page.mdx": "2025-05-07T15:35:11.029Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelReturnWorkflow/page.mdx": "2025-05-07T15:35:11.035Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createAndCompleteReturnOrderWorkflow/page.mdx": "2025-05-01T15:18:38.827Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createCompleteReturnValidationStep/page.mdx": "2025-02-11T11:36:42.766Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderPaymentCollectionWorkflow/page.mdx": "2025-05-01T15:18:38.121Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.declineOrderChangeWorkflow/page.mdx": "2025-05-01T15:18:38.133Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderChangeActionsWorkflow/page.mdx": "2025-05-01T15:18:38.135Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderChangeWorkflow/page.mdx": "2025-05-01T15:18:38.137Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderPaymentCollections/page.mdx": "2025-05-01T15:18:38.142Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.getOrderDetailWorkflow/page.mdx": "2025-05-01T15:18:38.394Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.markPaymentCollectionAsPaid/page.mdx": "2025-05-01T15:18:38.429Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveAndCompleteReturnOrderWorkflow/page.mdx": "2025-05-01T15:18:38.884Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderPaymentCollectionWorkflow/page.mdx": "2025-05-07T15:35:10.408Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.declineOrderChangeWorkflow/page.mdx": "2025-05-07T15:35:10.419Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderChangeActionsWorkflow/page.mdx": "2025-05-07T15:35:10.421Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderChangeWorkflow/page.mdx": "2025-05-07T15:35:10.423Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.deleteOrderPaymentCollections/page.mdx": "2025-05-07T15:35:10.428Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.getOrderDetailWorkflow/page.mdx": "2025-05-07T15:35:10.668Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.markPaymentCollectionAsPaid/page.mdx": "2025-05-07T15:35:10.703Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveAndCompleteReturnOrderWorkflow/page.mdx": "2025-05-07T15:35:11.154Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.receiveCompleteReturnValidationStep/page.mdx": "2025-04-11T09:04:39.668Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.throwUnlessPaymentCollectionNotPaid/page.mdx": "2025-01-20T08:25:21.432Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.throwUnlessStatusIsNotPaid/page.mdx": "2025-05-01T15:18:38.139Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.throwUnlessStatusIsNotPaid/page.mdx": "2025-05-07T15:35:10.426Z", "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.authorizePaymentSessionStep/page.mdx": "2025-02-11T11:36:43.569Z", "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.cancelPaymentStep/page.mdx": "2025-01-13T17:30:25.526Z", "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.capturePaymentStep/page.mdx": "2025-01-17T16:43:24.790Z", "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.refundPaymentStep/page.mdx": "2025-01-17T16:43:24.798Z", - "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.capturePaymentWorkflow/page.mdx": "2025-05-01T15:18:39.308Z", - "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createPaymentSessionStep/page.mdx": "2025-05-01T15:18:39.342Z", + "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.capturePaymentWorkflow/page.mdx": "2025-05-07T15:35:11.523Z", + "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createPaymentSessionStep/page.mdx": "2025-05-07T15:35:11.554Z", "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.createRefundReasonStep/page.mdx": "2025-04-11T09:04:40.525Z", - "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.deletePaymentSessionsStep/page.mdx": "2025-05-01T15:18:39.346Z", + "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.deletePaymentSessionsStep/page.mdx": "2025-05-07T15:35:11.558Z", "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.deleteRefundReasonsStep/page.mdx": "2025-01-13T17:30:25.585Z", "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.updatePaymentCollectionStep/page.mdx": "2025-04-11T09:04:40.543Z", - "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.updateRefundReasonsStep/page.mdx": "2025-05-01T15:18:39.357Z", + "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.updateRefundReasonsStep/page.mdx": "2025-05-07T15:35:11.567Z", "references/core_flows/Payment_Collection/Steps_Payment_Collection/functions/core_flows.Payment_Collection.Steps_Payment_Collection.validateDeletedPaymentSessionsStep/page.mdx": "2025-01-13T17:30:25.592Z", - "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.createPaymentSessionsWorkflow/page.mdx": "2025-05-01T15:18:39.365Z", - "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.createRefundReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.368Z", - "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.deletePaymentSessionsWorkflow/page.mdx": "2025-05-01T15:18:39.370Z", - "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.updateRefundReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.372Z", + "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.createPaymentSessionsWorkflow/page.mdx": "2025-05-07T15:35:11.575Z", + "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.createRefundReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.578Z", + "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.deletePaymentSessionsWorkflow/page.mdx": "2025-05-07T15:35:11.580Z", + "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.updateRefundReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.582Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.deletePriceListsStep/page.mdx": "2025-01-13T17:30:25.617Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.getExistingPriceListsPriceIdsStep/page.mdx": "2025-01-13T17:30:25.619Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.removePriceListPricesStep/page.mdx": "2025-01-13T17:30:25.620Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.updatePriceListsStep/page.mdx": "2025-04-11T09:04:40.617Z", - "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.validatePriceListsStep/page.mdx": "2025-05-01T15:18:39.397Z", + "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.validatePriceListsStep/page.mdx": "2025-05-07T15:35:11.605Z", "references/core_flows/Price_List/Steps_Price_List/functions/core_flows.Price_List.Steps_Price_List.validateVariantPriceLinksStep/page.mdx": "2025-01-13T17:30:25.630Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.deletePriceListsWorkflow/page.mdx": "2025-05-01T15:18:39.414Z", - "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.removePriceListPricesWorkflow/page.mdx": "2025-05-01T15:18:39.416Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.deletePriceListsWorkflow/page.mdx": "2025-05-07T15:35:11.622Z", + "references/core_flows/Price_List/Workflows_Price_List/functions/core_flows.Price_List.Workflows_Price_List.removePriceListPricesWorkflow/page.mdx": "2025-05-07T15:35:11.624Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.createPricePreferencesStep/page.mdx": "2025-04-11T09:04:40.679Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.createPriceSetsStep/page.mdx": "2025-04-11T09:04:40.675Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.deletePricePreferencesStep/page.mdx": "2025-01-13T17:30:25.659Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.updatePricePreferencesAsArrayStep/page.mdx": "2025-04-11T09:04:40.686Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.updatePricePreferencesStep/page.mdx": "2025-04-11T09:04:40.690Z", "references/core_flows/Pricing/Steps_Pricing/functions/core_flows.Pricing.Steps_Pricing.updatePriceSetsStep/page.mdx": "2025-04-11T09:04:40.696Z", - "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.createPricePreferencesWorkflow/page.mdx": "2025-05-01T15:18:39.439Z", - "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.deletePricePreferencesWorkflow/page.mdx": "2025-05-01T15:18:39.441Z", - "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.updatePricePreferencesWorkflow/page.mdx": "2025-05-01T15:18:39.443Z", + "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.createPricePreferencesWorkflow/page.mdx": "2025-05-07T15:35:11.648Z", + "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.deletePricePreferencesWorkflow/page.mdx": "2025-05-07T15:35:11.650Z", + "references/core_flows/Pricing/Workflows_Pricing/functions/core_flows.Pricing.Workflows_Pricing.updatePricePreferencesWorkflow/page.mdx": "2025-05-07T15:35:11.652Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.batchLinkProductsToCollectionStep/page.mdx": "2025-01-13T18:05:52.101Z", - "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.createCollectionsStep/page.mdx": "2025-05-01T15:18:39.454Z", + "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.createCollectionsStep/page.mdx": "2025-05-07T15:35:11.663Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.createProductOptionsStep/page.mdx": "2025-04-11T09:04:40.737Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.createProductTagsStep/page.mdx": "2025-04-11T09:04:40.811Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.createProductTypesStep/page.mdx": "2025-04-11T09:04:40.815Z", @@ -1882,13 +1882,13 @@ export const generatedEditDates = { "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.updateProductVariantsStep/page.mdx": "2025-04-11T09:04:40.800Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.updateProductsStep/page.mdx": "2025-04-11T09:04:40.784Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.waitConfirmationProductImportStep/page.mdx": "2025-01-13T17:30:25.739Z", - "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchLinkProductsToCategoryWorkflow/page.mdx": "2025-05-01T15:18:39.542Z", - "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchLinkProductsToCollectionWorkflow/page.mdx": "2025-05-01T15:18:39.530Z", + "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchLinkProductsToCategoryWorkflow/page.mdx": "2025-05-07T15:35:11.738Z", + "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchLinkProductsToCollectionWorkflow/page.mdx": "2025-05-07T15:35:11.733Z", "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchProductVariantsWorkflow/page.mdx": "2025-05-01T15:18:39.539Z", "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.batchProductsWorkflow/page.mdx": "2025-05-01T15:18:39.546Z", - "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.exportProductsWorkflow/page.mdx": "2025-05-01T15:18:39.680Z", + "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.exportProductsWorkflow/page.mdx": "2025-05-07T15:35:11.812Z", "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.importProductsWorkflow/page.mdx": "2025-05-01T15:18:39.683Z", - "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.upsertVariantPricesWorkflow/page.mdx": "2025-05-01T15:18:39.696Z", + "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.upsertVariantPricesWorkflow/page.mdx": "2025-05-07T15:35:11.825Z", "references/core_flows/Product_Category/Steps_Product_Category/functions/core_flows.Product_Category.Steps_Product_Category.createProductCategoriesStep/page.mdx": "2025-04-11T09:04:41.068Z", "references/core_flows/Product_Category/Steps_Product_Category/functions/core_flows.Product_Category.Steps_Product_Category.deleteProductCategoriesStep/page.mdx": "2025-01-13T17:30:25.836Z", "references/core_flows/Product_Category/Steps_Product_Category/functions/core_flows.Product_Category.Steps_Product_Category.updateProductCategoriesStep/page.mdx": "2025-04-11T09:04:41.084Z", @@ -1902,8 +1902,8 @@ export const generatedEditDates = { "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.deletePromotionsStep/page.mdx": "2025-01-13T17:30:25.862Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.removeCampaignPromotionsStep/page.mdx": "2025-02-11T11:36:44.182Z", "references/core_flows/Promotion/Steps_Promotion/functions/core_flows.Promotion.Steps_Promotion.updateCampaignsStep/page.mdx": "2025-04-11T09:04:41.149Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.addOrRemoveCampaignPromotionsWorkflow/page.mdx": "2025-05-01T15:18:39.746Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.batchPromotionRulesWorkflow/page.mdx": "2025-05-01T15:18:39.748Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.addOrRemoveCampaignPromotionsWorkflow/page.mdx": "2025-05-07T15:35:11.876Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.batchPromotionRulesWorkflow/page.mdx": "2025-05-07T15:35:11.879Z", "references/core_flows/Region/Steps_Region/functions/core_flows.Region.Steps_Region.createRegionsStep/page.mdx": "2025-04-11T09:04:41.230Z", "references/core_flows/Region/Steps_Region/functions/core_flows.Region.Steps_Region.deleteRegionsStep/page.mdx": "2025-01-13T17:30:25.910Z", "references/core_flows/Region/Steps_Region/functions/core_flows.Region.Steps_Region.updateRegionsStep/page.mdx": "2025-04-11T09:04:41.239Z", @@ -1911,16 +1911,16 @@ export const generatedEditDates = { "references/core_flows/Reservation/Steps_Reservation/functions/core_flows.Reservation.Steps_Reservation.deleteReservationsByLineItemsStep/page.mdx": "2025-01-13T17:30:25.927Z", "references/core_flows/Reservation/Steps_Reservation/functions/core_flows.Reservation.Steps_Reservation.deleteReservationsStep/page.mdx": "2025-01-13T17:30:25.928Z", "references/core_flows/Reservation/Steps_Reservation/functions/core_flows.Reservation.Steps_Reservation.updateReservationsStep/page.mdx": "2025-04-11T09:04:41.277Z", - "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.createReservationsWorkflow/page.mdx": "2025-05-01T15:18:39.806Z", - "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.deleteReservationsByLineItemsWorkflow/page.mdx": "2025-05-01T15:18:39.816Z", - "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.deleteReservationsWorkflow/page.mdx": "2025-05-01T15:18:39.808Z", - "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.updateReservationsWorkflow/page.mdx": "2025-05-01T15:18:39.825Z", + "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.createReservationsWorkflow/page.mdx": "2025-05-07T15:35:11.938Z", + "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.deleteReservationsByLineItemsWorkflow/page.mdx": "2025-05-07T15:35:11.942Z", + "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.deleteReservationsWorkflow/page.mdx": "2025-05-07T15:35:11.940Z", + "references/core_flows/Reservation/Workflows_Reservation/functions/core_flows.Reservation.Workflows_Reservation.updateReservationsWorkflow/page.mdx": "2025-05-07T15:35:11.945Z", "references/core_flows/Return_Reason/Steps_Return_Reason/functions/core_flows.Return_Reason.Steps_Return_Reason.createReturnReasonsStep/page.mdx": "2025-04-11T09:04:41.309Z", "references/core_flows/Return_Reason/Steps_Return_Reason/functions/core_flows.Return_Reason.Steps_Return_Reason.deleteReturnReasonStep/page.mdx": "2025-01-13T17:30:25.948Z", "references/core_flows/Return_Reason/Steps_Return_Reason/functions/core_flows.Return_Reason.Steps_Return_Reason.updateReturnReasonsStep/page.mdx": "2025-04-11T09:04:41.317Z", - "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.createReturnReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.838Z", - "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.deleteReturnReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.840Z", - "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.updateReturnReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.844Z", + "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.createReturnReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.957Z", + "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.deleteReturnReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.959Z", + "references/core_flows/Return_Reason/Workflows_Return_Reason/functions/core_flows.Return_Reason.Workflows_Return_Reason.updateReturnReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.963Z", "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.associateLocationsWithSalesChannelsStep/page.mdx": "2025-01-13T17:30:25.974Z", "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.associateProductsWithSalesChannelsStep/page.mdx": "2025-01-13T17:30:25.964Z", "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.createDefaultSalesChannelStep/page.mdx": "2025-01-13T18:05:52.383Z", @@ -1929,33 +1929,33 @@ export const generatedEditDates = { "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.detachLocationsFromSalesChannelsStep/page.mdx": "2025-01-13T17:30:25.977Z", "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.detachProductsFromSalesChannelsStep/page.mdx": "2025-01-13T17:30:25.971Z", "references/core_flows/Sales_Channel/Steps_Sales_Channel/functions/core_flows.Sales_Channel.Steps_Sales_Channel.updateSalesChannelsStep/page.mdx": "2025-04-11T09:04:41.362Z", - "references/core_flows/Sales_Channel/Workflows_Sales_Channel/functions/core_flows.Sales_Channel.Workflows_Sales_Channel.linkProductsToSalesChannelWorkflow/page.mdx": "2025-05-01T15:18:39.861Z", + "references/core_flows/Sales_Channel/Workflows_Sales_Channel/functions/core_flows.Sales_Channel.Workflows_Sales_Channel.linkProductsToSalesChannelWorkflow/page.mdx": "2025-05-07T15:35:11.983Z", "references/core_flows/Shipping_Profile/Steps_Shipping_Profile/functions/core_flows.Shipping_Profile.Steps_Shipping_Profile.deleteShippingProfilesStep/page.mdx": "2025-01-13T17:30:25.996Z", - "references/core_flows/Shipping_Profile/Workflows_Shipping_Profile/functions/core_flows.Shipping_Profile.Workflows_Shipping_Profile.deleteShippingProfileWorkflow/page.mdx": "2025-05-01T15:18:39.884Z", - "references/core_flows/Stock_Location/Steps_Stock_Location/functions/core_flows.Stock_Location.Steps_Stock_Location.createStockLocations/page.mdx": "2025-05-01T15:18:39.888Z", + "references/core_flows/Shipping_Profile/Workflows_Shipping_Profile/functions/core_flows.Shipping_Profile.Workflows_Shipping_Profile.deleteShippingProfileWorkflow/page.mdx": "2025-05-07T15:35:12.005Z", + "references/core_flows/Stock_Location/Steps_Stock_Location/functions/core_flows.Stock_Location.Steps_Stock_Location.createStockLocations/page.mdx": "2025-05-07T15:35:12.009Z", "references/core_flows/Stock_Location/Steps_Stock_Location/functions/core_flows.Stock_Location.Steps_Stock_Location.deleteStockLocationsStep/page.mdx": "2025-01-13T17:30:26.004Z", "references/core_flows/Stock_Location/Steps_Stock_Location/functions/core_flows.Stock_Location.Steps_Stock_Location.updateStockLocationsStep/page.mdx": "2025-04-11T09:04:41.433Z", - "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.createLocationFulfillmentSetWorkflow/page.mdx": "2025-05-01T15:18:39.895Z", + "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.createLocationFulfillmentSetWorkflow/page.mdx": "2025-05-07T15:35:12.017Z", "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.createStockLocationsWorkflow/page.mdx": "2025-05-01T15:18:39.900Z", - "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.deleteStockLocationsWorkflow/page.mdx": "2025-05-01T15:18:39.902Z", - "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.linkSalesChannelsToStockLocationWorkflow/page.mdx": "2025-05-01T15:18:39.904Z", - "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.updateStockLocationsWorkflow/page.mdx": "2025-05-01T15:18:39.908Z", + "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.deleteStockLocationsWorkflow/page.mdx": "2025-05-07T15:35:12.023Z", + "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.linkSalesChannelsToStockLocationWorkflow/page.mdx": "2025-05-07T15:35:12.025Z", + "references/core_flows/Stock_Location/Workflows_Stock_Location/functions/core_flows.Stock_Location.Workflows_Stock_Location.updateStockLocationsWorkflow/page.mdx": "2025-05-07T15:35:12.029Z", "references/core_flows/Store/Steps_Store/functions/core_flows.Store.Steps_Store.createStoresStep/page.mdx": "2025-04-11T09:04:41.466Z", "references/core_flows/Store/Steps_Store/functions/core_flows.Store.Steps_Store.deleteStoresStep/page.mdx": "2025-01-13T17:30:26.026Z", "references/core_flows/Store/Steps_Store/functions/core_flows.Store.Steps_Store.updateStoresStep/page.mdx": "2025-04-11T09:04:41.473Z", - "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.createStoresWorkflow/page.mdx": "2025-05-01T15:18:39.917Z", - "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.deleteStoresWorkflow/page.mdx": "2025-05-01T15:18:39.919Z", - "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.updateStoresWorkflow/page.mdx": "2025-05-01T15:18:39.923Z", + "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.createStoresWorkflow/page.mdx": "2025-05-07T15:35:12.041Z", + "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.deleteStoresWorkflow/page.mdx": "2025-05-07T15:35:12.044Z", + "references/core_flows/Store/Workflows_Store/functions/core_flows.Store.Workflows_Store.updateStoresWorkflow/page.mdx": "2025-05-07T15:35:12.047Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.createTaxRegionsStep/page.mdx": "2025-04-11T09:04:41.494Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.deleteTaxRateRulesStep/page.mdx": "2025-01-13T17:30:26.048Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.deleteTaxRatesStep/page.mdx": "2025-01-13T17:30:26.049Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.deleteTaxRegionsStep/page.mdx": "2025-01-13T17:30:26.043Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.listTaxRateIdsStep/page.mdx": "2025-01-13T18:05:52.483Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.listTaxRateRuleIdsStep/page.mdx": "2025-01-13T18:05:52.486Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRegionsWorkflow/page.mdx": "2025-05-01T15:18:39.962Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRateRulesWorkflow/page.mdx": "2025-05-01T15:18:39.964Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRatesWorkflow/page.mdx": "2025-05-01T15:18:39.966Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRegionsWorkflow/page.mdx": "2025-05-01T15:18:39.967Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.createTaxRegionsWorkflow/page.mdx": "2025-05-07T15:35:12.090Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRateRulesWorkflow/page.mdx": "2025-05-07T15:35:12.092Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRatesWorkflow/page.mdx": "2025-05-07T15:35:12.094Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.deleteTaxRegionsWorkflow/page.mdx": "2025-05-07T15:35:12.095Z", "references/core_flows/User/Steps_User/functions/core_flows.User.Steps_User.createUsersStep/page.mdx": "2025-04-11T09:04:41.603Z", "references/core_flows/User/Steps_User/functions/core_flows.User.Steps_User.deleteUsersStep/page.mdx": "2025-01-13T17:30:26.110Z", "references/core_flows/User/Steps_User/functions/core_flows.User.Steps_User.updateUsersStep/page.mdx": "2025-04-11T09:04:41.611Z", @@ -2060,8 +2060,8 @@ export const generatedEditDates = { "references/region/interfaces/region.MessageAggregatorFormat/page.mdx": "2024-12-09T13:22:04.016Z", "references/region/interfaces/region.PaymentProviderDTO/page.mdx": "2024-12-09T13:22:03.956Z", "references/region/interfaces/region.RegionDTO/page.mdx": "2024-12-09T13:22:03.956Z", - "references/region_models/variables/region_models.Country/page.mdx": "2025-05-01T15:18:48.389Z", - "references/region_models/variables/region_models.Region/page.mdx": "2025-05-01T15:18:48.391Z", + "references/region_models/variables/region_models.Country/page.mdx": "2025-05-07T15:35:20.624Z", + "references/region_models/variables/region_models.Region/page.mdx": "2025-05-07T15:35:20.626Z", "references/sales_channel/IMessageAggregator/methods/sales_channel.IMessageAggregator.getMessages/page.mdx": "2025-04-11T09:04:52.240Z", "references/sales_channel/interfaces/sales_channel.IModuleService/page.mdx": "2024-12-09T13:22:04.060Z", "references/sales_channel/interfaces/sales_channel.MessageAggregatorFormat/page.mdx": "2024-12-09T13:22:04.104Z", @@ -2623,8 +2623,8 @@ export const generatedEditDates = { "references/js_sdk/store/Store/properties/js_sdk.store.Store.region/page.mdx": "2025-01-13T18:05:59.452Z", "references/js_sdk/store/classes/js_sdk.store.Store/page.mdx": "2024-11-25T17:49:55.112Z", "references/modules/js_sdk/page.mdx": "2024-10-22T15:09:52.263Z", - "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.validateInventoryDeleteStep/page.mdx": "2025-05-01T15:18:37.293Z", - "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.updateTaxRegionsWorkflow/page.mdx": "2025-05-01T15:18:39.978Z", + "references/core_flows/Inventory/Steps_Inventory/functions/core_flows.Inventory.Steps_Inventory.validateInventoryDeleteStep/page.mdx": "2025-05-07T15:35:09.590Z", + "references/core_flows/Tax/Workflows_Tax/functions/core_flows.Tax.Workflows_Tax.updateTaxRegionsWorkflow/page.mdx": "2025-05-07T15:35:12.107Z", "references/core_flows/interfaces/core_flows.ValidateInventoryDeleteStepInput/page.mdx": "2024-12-09T13:21:50.648Z", "references/core_flows/types/core_flows.UseQueryGraphStepInput/page.mdx": "2024-12-09T13:21:50.232Z", "references/helper_steps/types/helper_steps.UseQueryGraphStepInput/page.mdx": "2024-12-09T13:21:56.600Z", @@ -3027,7 +3027,7 @@ export const generatedEditDates = { "references/types/interfaces/types.BaseProductVariantParams/page.mdx": "2025-01-13T18:05:55.128Z", "references/workflows/types/workflows.UnwrapWorkflowInputDataType/page.mdx": "2025-01-13T17:30:31.439Z", "references/core_flows/Customer_Group/Steps_Customer_Group/functions/core_flows.Customer_Group.Steps_Customer_Group.linkCustomerGroupsToCustomerStep/page.mdx": "2025-01-13T18:05:49.504Z", - "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.linkCustomerGroupsToCustomerWorkflow/page.mdx": "2025-05-01T15:18:36.803Z", + "references/core_flows/Customer_Group/Workflows_Customer_Group/functions/core_flows.Customer_Group.Workflows_Customer_Group.linkCustomerGroupsToCustomerWorkflow/page.mdx": "2025-05-07T15:35:08.997Z", "references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/page.mdx": "2025-04-11T09:04:54.008Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminBatchPriceListPrice/page.mdx": "2024-12-09T13:21:34.169Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminBatchProductResponse/page.mdx": "2025-04-11T09:04:47.346Z", @@ -3299,14 +3299,14 @@ export const generatedEditDates = { "references/pricing/types/pricing.FilterQueryProperties/page.mdx": "2024-12-23T12:30:30.704Z", "references/product/types/product.Constructor/page.mdx": "2024-12-09T13:22:03.168Z", "references/product/types/product.FilterQueryProperties/page.mdx": "2024-12-23T12:30:30.868Z", - "references/product_models/variables/product_models.Product/page.mdx": "2025-05-01T15:18:48.357Z", - "references/product_models/variables/product_models.ProductCategory/page.mdx": "2025-05-01T15:18:48.307Z", - "references/product_models/variables/product_models.ProductCollection/page.mdx": "2025-05-01T15:18:48.312Z", - "references/product_models/variables/product_models.ProductOption/page.mdx": "2025-05-01T15:18:48.327Z", - "references/product_models/variables/product_models.ProductOptionValue/page.mdx": "2025-05-01T15:18:48.322Z", - "references/product_models/variables/product_models.ProductTag/page.mdx": "2025-05-01T15:18:48.332Z", - "references/product_models/variables/product_models.ProductType/page.mdx": "2025-05-01T15:18:48.336Z", - "references/product_models/variables/product_models.ProductVariant/page.mdx": "2025-05-01T15:18:48.343Z", + "references/product_models/variables/product_models.Product/page.mdx": "2025-05-07T15:35:20.591Z", + "references/product_models/variables/product_models.ProductCategory/page.mdx": "2025-05-07T15:35:20.540Z", + "references/product_models/variables/product_models.ProductCollection/page.mdx": "2025-05-07T15:35:20.544Z", + "references/product_models/variables/product_models.ProductOption/page.mdx": "2025-05-07T15:35:20.561Z", + "references/product_models/variables/product_models.ProductOptionValue/page.mdx": "2025-05-07T15:35:20.555Z", + "references/product_models/variables/product_models.ProductTag/page.mdx": "2025-05-07T15:35:20.566Z", + "references/product_models/variables/product_models.ProductType/page.mdx": "2025-05-07T15:35:20.570Z", + "references/product_models/variables/product_models.ProductVariant/page.mdx": "2025-05-07T15:35:20.577Z", "references/promotion/types/promotion.Constructor/page.mdx": "2024-12-09T13:22:03.708Z", "references/promotion/types/promotion.FilterQueryProperties/page.mdx": "2024-12-23T12:30:31.117Z", "references/region/types/region.Constructor/page.mdx": "2024-12-09T13:22:03.944Z", @@ -3319,7 +3319,7 @@ export const generatedEditDates = { "references/types/DmlTypes/types/types.DmlTypes.IsNullableRelation/page.mdx": "2024-12-09T13:21:32.993Z", "references/types/types/types.FilterQueryProperties/page.mdx": "2024-12-23T12:30:28.129Z", "references/cart/types/cart.FilterQuery/page.mdx": "2024-11-27T16:33:41.056Z", - "references/currency_models/variables/currency_models.Currency/page.mdx": "2025-05-01T15:18:46.211Z", + "references/currency_models/variables/currency_models.Currency/page.mdx": "2025-05-07T15:35:18.429Z", "references/customer/types/customer.FilterQuery/page.mdx": "2024-11-27T16:33:52.327Z", "references/fulfillment/types/fulfillment.FilterQuery/page.mdx": "2024-11-27T16:33:52.465Z", "references/inventory_next/types/inventory_next.FilterQuery/page.mdx": "2024-11-27T16:33:52.961Z", @@ -3335,8 +3335,8 @@ export const generatedEditDates = { "references/types/DmlTypes/types/types.DmlTypes.InferForeignKeys/page.mdx": "2025-02-24T10:48:36.703Z", "references/types/DmlTypes/types/types.DmlTypes.InferSchemaFields/page.mdx": "2025-01-13T18:05:54.083Z", "references/types/interfaces/types.BaseRepositoryService/page.mdx": "2024-12-09T13:21:32.969Z", - "references/auth_models/variables/auth_models.AuthIdentity/page.mdx": "2025-05-01T15:18:46.149Z", - "references/auth_models/variables/auth_models.ProviderIdentity/page.mdx": "2025-05-01T15:18:46.153Z", + "references/auth_models/variables/auth_models.AuthIdentity/page.mdx": "2025-05-07T15:35:18.376Z", + "references/auth_models/variables/auth_models.ProviderIdentity/page.mdx": "2025-05-07T15:35:18.377Z", "references/dml/entity/DmlEntity/methods/dml.entity.DmlEntity.checks/page.mdx": "2024-12-09T13:21:55.464Z", "references/helper_steps/functions/helper_steps.validatePresenceOfStep/page.mdx": "2025-01-13T17:30:22.855Z", "references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/page.mdx": "2025-04-11T09:04:55.384Z", @@ -3345,13 +3345,13 @@ export const generatedEditDates = { "references/order/types/order.OrderChangeType/page.mdx": "2025-01-07T12:54:23.927Z", "references/pricing/interfaces/pricing.RuleWithOperator/page.mdx": "2025-01-13T17:30:30.399Z", "references/pricing/types/pricing.PricingRuleOperatorValues/page.mdx": "2024-12-04T18:29:34.568Z", - "references/pricing_models/variables/pricing_models.Price/page.mdx": "2025-05-01T15:18:48.296Z", - "references/pricing_models/variables/pricing_models.PriceList/page.mdx": "2025-05-01T15:18:48.286Z", - "references/pricing_models/variables/pricing_models.PriceListRule/page.mdx": "2025-05-01T15:18:48.282Z", - "references/pricing_models/variables/pricing_models.PricePreference/page.mdx": "2025-05-01T15:18:48.287Z", - "references/pricing_models/variables/pricing_models.PriceRule/page.mdx": "2025-05-01T15:18:48.289Z", - "references/pricing_models/variables/pricing_models.PriceSet/page.mdx": "2025-05-01T15:18:48.292Z", - "references/product_models/variables/product_models.ProductImage/page.mdx": "2025-05-01T15:18:48.317Z", + "references/pricing_models/variables/pricing_models.Price/page.mdx": "2025-05-07T15:35:20.529Z", + "references/pricing_models/variables/pricing_models.PriceList/page.mdx": "2025-05-07T15:35:20.516Z", + "references/pricing_models/variables/pricing_models.PriceListRule/page.mdx": "2025-05-07T15:35:20.512Z", + "references/pricing_models/variables/pricing_models.PricePreference/page.mdx": "2025-05-07T15:35:20.517Z", + "references/pricing_models/variables/pricing_models.PriceRule/page.mdx": "2025-05-07T15:35:20.520Z", + "references/pricing_models/variables/pricing_models.PriceSet/page.mdx": "2025-05-07T15:35:20.524Z", + "references/product_models/variables/product_models.ProductImage/page.mdx": "2025-05-07T15:35:20.550Z", "references/types/DmlTypes/types/types.DmlTypes.CheckConstraint/page.mdx": "2025-01-13T18:05:54.113Z", "references/types/DmlTypes/types/types.DmlTypes.InferCheckConstraintsProperties/page.mdx": "2025-01-13T18:05:54.110Z", "references/types/DmlTypes/types/types.DmlTypes.InferSchemaProperties/page.mdx": "2025-01-13T18:05:54.102Z", @@ -3359,8 +3359,8 @@ export const generatedEditDates = { "references/types/HttpTypes/interfaces/types.HttpTypes.OrderAddress/page.mdx": "2024-12-09T13:21:33.949Z", "references/types/WorkflowTypes/OrderWorkflow/types/types.WorkflowTypes.OrderWorkflow.UpdateOrderShippingAddressWorkflowInput/page.mdx": "2025-01-13T17:30:29.222Z", "references/types/WorkflowTypes/OrderWorkflow/types/types.WorkflowTypes.OrderWorkflow.UpdateOrderWorkflowInput/page.mdx": "2025-02-11T11:36:51.229Z", - "references/user_models/variables/user_models.Invite/page.mdx": "2025-05-01T15:18:48.416Z", - "references/user_models/variables/user_models.User/page.mdx": "2025-05-01T15:18:48.417Z", + "references/user_models/variables/user_models.Invite/page.mdx": "2025-05-07T15:35:20.665Z", + "references/user_models/variables/user_models.User/page.mdx": "2025-05-07T15:35:20.666Z", "references/utils/PricingUtils/enums/utils.PricingUtils.PriceListStatus/page.mdx": "2024-12-04T18:29:19.807Z", "references/utils/PricingUtils/enums/utils.PricingUtils.PriceListType/page.mdx": "2024-12-04T18:29:19.808Z", "references/utils/PricingUtils/enums/utils.PricingUtils.PricingRuleOperator/page.mdx": "2024-12-04T18:29:19.806Z", @@ -3370,17 +3370,17 @@ export const generatedEditDates = { "references/order/interfaces/order.CreateOrderChangeActionDTO/page.mdx": "2024-12-09T13:22:01.316Z", "references/types/DmlTypes/types/types.DmlTypes.EntityIndex/page.mdx": "2025-01-13T18:05:54.118Z", "references/types/DmlTypes/types/types.DmlTypes.InferIndexableProperties/page.mdx": "2025-01-13T18:05:54.109Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.transferCartCustomerWorkflow/page.mdx": "2025-05-01T15:18:36.680Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.transferCartCustomerWorkflow/page.mdx": "2025-05-07T15:35:08.865Z", "references/core_flows/Common/Steps_Common/functions/core_flows.Common.Steps_Common.validatePresenceOfStep/page.mdx": "2025-01-13T17:30:23.159Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.buildPriceSet/page.mdx": "2024-12-10T14:54:58.496Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.registerOrderChangesStep/page.mdx": "2025-01-13T17:30:23.749Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.updateOrdersStep/page.mdx": "2025-04-11T09:04:37.245Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.acceptOrderTransferValidationStep/page.mdx": "2025-04-11T09:04:40.225Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.acceptOrderTransferWorkflow/page.mdx": "2025-05-01T15:18:39.118Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderTransferRequestWorkflow/page.mdx": "2025-05-01T15:18:39.128Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.acceptOrderTransferWorkflow/page.mdx": "2025-05-07T15:35:11.388Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelOrderTransferRequestWorkflow/page.mdx": "2025-05-07T15:35:11.399Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.cancelTransferOrderRequestValidationStep/page.mdx": "2025-04-11T09:04:40.272Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderWorkflow/page.mdx": "2025-05-01T15:18:38.097Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.declineOrderTransferRequestWorkflow/page.mdx": "2025-05-01T15:18:39.138Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderWorkflow/page.mdx": "2025-05-07T15:35:10.385Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.declineOrderTransferRequestWorkflow/page.mdx": "2025-05-07T15:35:11.408Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.declineTransferOrderRequestValidationStep/page.mdx": "2025-04-11T09:04:40.292Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestOrderTransferValidationStep/page.mdx": "2025-04-11T09:04:40.179Z", "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.requestOrderTransferWorkflow/page.mdx": "2025-05-01T15:18:39.159Z", @@ -5532,20 +5532,20 @@ export const generatedEditDates = { "references/workflows/interfaces/workflows.ApplyStepOptions/page.mdx": "2025-01-13T17:30:31.420Z", "references/workflows/types/workflows.WorkflowData/page.mdx": "2024-12-23T13:57:08.059Z", "app/integrations/guides/resend/page.mdx": "2025-05-01T15:33:31.147Z", - "references/api_key_models/variables/api_key_models.ApiKey/page.mdx": "2025-05-01T15:18:46.147Z", + "references/api_key_models/variables/api_key_models.ApiKey/page.mdx": "2025-05-07T15:35:18.374Z", "references/cart/ICartModuleService/methods/cart.ICartModuleService.updateShippingMethods/page.mdx": "2025-04-11T09:04:44.258Z", "references/cart/interfaces/cart.UpdateShippingMethodDTO/page.mdx": "2024-12-10T14:54:57.530Z", - "references/cart_models/variables/cart_models.Address/page.mdx": "2025-05-01T15:18:46.162Z", - "references/cart_models/variables/cart_models.Cart/page.mdx": "2025-05-01T15:18:46.174Z", - "references/cart_models/variables/cart_models.LineItem/page.mdx": "2025-05-01T15:18:46.196Z", - "references/cart_models/variables/cart_models.LineItemAdjustment/page.mdx": "2025-05-01T15:18:46.183Z", - "references/cart_models/variables/cart_models.LineItemTaxLine/page.mdx": "2025-05-01T15:18:46.187Z", - "references/cart_models/variables/cart_models.ShippingMethod/page.mdx": "2025-05-01T15:18:46.209Z", - "references/cart_models/variables/cart_models.ShippingMethodAdjustment/page.mdx": "2025-05-01T15:18:46.199Z", - "references/cart_models/variables/cart_models.ShippingMethodTaxLine/page.mdx": "2025-05-01T15:18:46.202Z", + "references/cart_models/variables/cart_models.Address/page.mdx": "2025-05-07T15:35:18.379Z", + "references/cart_models/variables/cart_models.Cart/page.mdx": "2025-05-07T15:35:18.389Z", + "references/cart_models/variables/cart_models.LineItem/page.mdx": "2025-05-07T15:35:18.413Z", + "references/cart_models/variables/cart_models.LineItemAdjustment/page.mdx": "2025-05-07T15:35:18.399Z", + "references/cart_models/variables/cart_models.LineItemTaxLine/page.mdx": "2025-05-07T15:35:18.404Z", + "references/cart_models/variables/cart_models.ShippingMethod/page.mdx": "2025-05-07T15:35:18.427Z", + "references/cart_models/variables/cart_models.ShippingMethodAdjustment/page.mdx": "2025-05-07T15:35:18.417Z", + "references/cart_models/variables/cart_models.ShippingMethodTaxLine/page.mdx": "2025-05-07T15:35:18.420Z", "references/dml/Property/methods/dml.Property.autoincrement/page.mdx": "2024-12-10T14:55:08.589Z", "references/dml/dml.Property/page.mdx": "2024-12-10T14:55:08.588Z", - "references/sales_channel_models/variables/sales_channel_models.SalesChannel/page.mdx": "2025-05-01T15:18:48.392Z", + "references/sales_channel_models/variables/sales_channel_models.SalesChannel/page.mdx": "2025-05-07T15:35:20.627Z", "references/store/types/store.Constructor/page.mdx": "2024-12-10T14:55:13.404Z", "references/store/types/store.ExpandScalar/page.mdx": "2024-12-10T14:55:13.391Z", "references/store/types/store.FilterQuery/page.mdx": "2024-12-10T14:55:13.397Z", @@ -5558,8 +5558,8 @@ export const generatedEditDates = { "references/store/types/store.Query/page.mdx": "2024-12-10T14:55:13.390Z", "references/store/types/store.ReadonlyPrimary/page.mdx": "2024-12-10T14:55:13.392Z", "references/store/types/store.Scalar/page.mdx": "2024-12-10T14:55:13.391Z", - "references/store_models/variables/store_models.Store/page.mdx": "2025-05-01T15:18:48.400Z", - "references/store_models/variables/store_models.StoreCurrency/page.mdx": "2025-05-01T15:18:48.398Z", + "references/store_models/variables/store_models.Store/page.mdx": "2025-05-07T15:35:20.636Z", + "references/store_models/variables/store_models.StoreCurrency/page.mdx": "2025-05-07T15:35:20.634Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StoreCalculatedPrice/page.mdx": "2025-03-04T13:33:51.211Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StorePrice/page.mdx": "2024-12-10T14:54:56.154Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StorePriceRule/page.mdx": "2024-12-10T14:54:56.155Z", @@ -5575,16 +5575,16 @@ export const generatedEditDates = { "app/recipes/commerce-automation/restock-notification/page.mdx": "2025-04-17T08:48:39.058Z", "app/integrations/guides/shipstation/page.mdx": "2025-02-26T11:21:46.879Z", "app/nextjs-starter/guides/customize-stripe/page.mdx": "2024-12-25T14:48:55.877Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWithPricingWorkflow/page.mdx": "2025-05-01T15:18:36.664Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWithPricingWorkflow/page.mdx": "2025-05-07T15:35:08.847Z", "references/core_flows/Cart/Workflows_Cart/variables/core_flows.Cart.Workflows_Cart.listShippingOptionsForCartWithPricingWorkflowId/page.mdx": "2024-12-17T16:57:22.044Z", "references/core_flows/Fulfillment/Steps_Fulfillment/functions/core_flows.Fulfillment.Steps_Fulfillment.calculateShippingOptionsPricesStep/page.mdx": "2025-04-11T09:04:36.188Z", "references/core_flows/Fulfillment/Steps_Fulfillment/variables/core_flows.Fulfillment.Steps_Fulfillment.calculateShippingOptionsPricesStepId/page.mdx": "2024-12-17T16:57:22.226Z", - "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.calculateShippingOptionsPricesWorkflow/page.mdx": "2025-05-01T15:18:37.205Z", + "references/core_flows/Fulfillment/Workflows_Fulfillment/functions/core_flows.Fulfillment.Workflows_Fulfillment.calculateShippingOptionsPricesWorkflow/page.mdx": "2025-05-07T15:35:09.503Z", "references/core_flows/Fulfillment/Workflows_Fulfillment/variables/core_flows.Fulfillment.Workflows_Fulfillment.calculateShippingOptionsPricesWorkflowId/page.mdx": "2024-12-17T16:57:22.302Z", - "references/customer_models/variables/customer_models.Customer/page.mdx": "2025-05-01T15:18:46.224Z", - "references/customer_models/variables/customer_models.CustomerAddress/page.mdx": "2025-05-01T15:18:46.215Z", - "references/customer_models/variables/customer_models.CustomerGroup/page.mdx": "2025-05-01T15:18:46.221Z", - "references/customer_models/variables/customer_models.CustomerGroupCustomer/page.mdx": "2025-05-01T15:18:46.218Z", + "references/customer_models/variables/customer_models.Customer/page.mdx": "2025-05-07T15:35:18.443Z", + "references/customer_models/variables/customer_models.CustomerAddress/page.mdx": "2025-05-07T15:35:18.433Z", + "references/customer_models/variables/customer_models.CustomerGroup/page.mdx": "2025-05-07T15:35:18.439Z", + "references/customer_models/variables/customer_models.CustomerGroupCustomer/page.mdx": "2025-05-07T15:35:18.436Z", "references/dml/Property_Configuration_Methods/methods/dml.Property_Configuration_Methods.computed/page.mdx": "2024-12-17T16:57:24.854Z", "references/dml/Property_Types/methods/dml.Property_Types.float/page.mdx": "2024-12-17T16:57:24.842Z", "references/fulfillment/IFulfillmentModuleService/methods/fulfillment.IFulfillmentModuleService.calculateShippingOptionsPrices/page.mdx": "2025-04-11T09:04:45.482Z", @@ -5603,28 +5603,28 @@ export const generatedEditDates = { "references/fulfillment/interfaces/fulfillment.ShippingMethodTaxLineDTO/page.mdx": "2024-12-23T13:57:04.500Z", "references/fulfillment/interfaces/fulfillment.TaxLineDTO/page.mdx": "2024-12-17T16:57:24.925Z", "references/fulfillment/types/fulfillment.CalculatedShippingOptionPrice/page.mdx": "2025-01-17T16:43:27.044Z", - "references/inventory_next_models/variables/inventory_next_models.InventoryItem/page.mdx": "2025-05-01T15:18:46.346Z", - "references/inventory_next_models/variables/inventory_next_models.InventoryLevel/page.mdx": "2025-05-01T15:18:46.350Z", - "references/inventory_next_models/variables/inventory_next_models.ReservationItem/page.mdx": "2025-05-01T15:18:46.353Z", - "references/payment_models/variables/payment_models.Capture/page.mdx": "2025-05-01T15:18:48.254Z", - "references/payment_models/variables/payment_models.Payment/page.mdx": "2025-05-01T15:18:48.273Z", - "references/payment_models/variables/payment_models.PaymentCollection/page.mdx": "2025-05-01T15:18:48.259Z", - "references/payment_models/variables/payment_models.PaymentProvider/page.mdx": "2025-05-01T15:18:48.262Z", - "references/payment_models/variables/payment_models.PaymentSession/page.mdx": "2025-05-01T15:18:48.267Z", - "references/payment_models/variables/payment_models.Refund/page.mdx": "2025-05-01T15:18:48.279Z", - "references/payment_models/variables/payment_models.RefundReason/page.mdx": "2025-05-01T15:18:48.275Z", - "references/promotion_models/variables/promotion_models.ApplicationMethod/page.mdx": "2025-05-01T15:18:48.365Z", - "references/promotion_models/variables/promotion_models.Campaign/page.mdx": "2025-05-01T15:18:48.371Z", - "references/promotion_models/variables/promotion_models.CampaignBudget/page.mdx": "2025-05-01T15:18:48.367Z", - "references/promotion_models/variables/promotion_models.Promotion/page.mdx": "2025-05-01T15:18:48.386Z", - "references/promotion_models/variables/promotion_models.PromotionRule/page.mdx": "2025-05-01T15:18:48.380Z", - "references/promotion_models/variables/promotion_models.PromotionRuleValue/page.mdx": "2025-05-01T15:18:48.374Z", - "references/stock_location_next_models/variables/stock_location_next_models.StockLocation/page.mdx": "2025-05-01T15:18:48.396Z", - "references/stock_location_next_models/variables/stock_location_next_models.StockLocationAddress/page.mdx": "2025-05-01T15:18:48.395Z", - "references/tax_models/variables/tax_models.TaxProvider/page.mdx": "2025-05-01T15:18:48.403Z", - "references/tax_models/variables/tax_models.TaxRate/page.mdx": "2025-05-01T15:18:48.409Z", - "references/tax_models/variables/tax_models.TaxRateRule/page.mdx": "2025-05-01T15:18:48.405Z", - "references/tax_models/variables/tax_models.TaxRegion/page.mdx": "2025-05-01T15:18:48.414Z", + "references/inventory_next_models/variables/inventory_next_models.InventoryItem/page.mdx": "2025-05-07T15:35:18.567Z", + "references/inventory_next_models/variables/inventory_next_models.InventoryLevel/page.mdx": "2025-05-07T15:35:18.571Z", + "references/inventory_next_models/variables/inventory_next_models.ReservationItem/page.mdx": "2025-05-07T15:35:18.574Z", + "references/payment_models/variables/payment_models.Capture/page.mdx": "2025-05-07T15:35:20.483Z", + "references/payment_models/variables/payment_models.Payment/page.mdx": "2025-05-07T15:35:20.504Z", + "references/payment_models/variables/payment_models.PaymentCollection/page.mdx": "2025-05-07T15:35:20.489Z", + "references/payment_models/variables/payment_models.PaymentProvider/page.mdx": "2025-05-07T15:35:20.492Z", + "references/payment_models/variables/payment_models.PaymentSession/page.mdx": "2025-05-07T15:35:20.497Z", + "references/payment_models/variables/payment_models.Refund/page.mdx": "2025-05-07T15:35:20.509Z", + "references/payment_models/variables/payment_models.RefundReason/page.mdx": "2025-05-07T15:35:20.506Z", + "references/promotion_models/variables/promotion_models.ApplicationMethod/page.mdx": "2025-05-07T15:35:20.600Z", + "references/promotion_models/variables/promotion_models.Campaign/page.mdx": "2025-05-07T15:35:20.605Z", + "references/promotion_models/variables/promotion_models.CampaignBudget/page.mdx": "2025-05-07T15:35:20.602Z", + "references/promotion_models/variables/promotion_models.Promotion/page.mdx": "2025-05-07T15:35:20.621Z", + "references/promotion_models/variables/promotion_models.PromotionRule/page.mdx": "2025-05-07T15:35:20.615Z", + "references/promotion_models/variables/promotion_models.PromotionRuleValue/page.mdx": "2025-05-07T15:35:20.608Z", + "references/stock_location_next_models/variables/stock_location_next_models.StockLocation/page.mdx": "2025-05-07T15:35:20.632Z", + "references/stock_location_next_models/variables/stock_location_next_models.StockLocationAddress/page.mdx": "2025-05-07T15:35:20.630Z", + "references/tax_models/variables/tax_models.TaxProvider/page.mdx": "2025-05-07T15:35:20.639Z", + "references/tax_models/variables/tax_models.TaxRate/page.mdx": "2025-05-07T15:35:20.656Z", + "references/tax_models/variables/tax_models.TaxRateRule/page.mdx": "2025-05-07T15:35:20.641Z", + "references/tax_models/variables/tax_models.TaxRegion/page.mdx": "2025-05-07T15:35:20.662Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StoreShippingOptionResponse/page.mdx": "2025-04-11T09:04:47.640Z", "references/types/HttpTypes/types/types.HttpTypes.StoreCalculateShippingOptionPrice/page.mdx": "2025-04-11T09:04:47.634Z", "references/types/WorkflowTypes/FulfillmentWorkflow/types/types.WorkflowTypes.FulfillmentWorkflow.CalculateShippingOptionsPricesWorkflowInput/page.mdx": "2024-12-17T16:57:21.219Z", @@ -5645,18 +5645,18 @@ export const generatedEditDates = { "references/core_flows/interfaces/core_flows.GetItemTaxLinesStepInput/page.mdx": "2025-04-11T09:04:43.408Z", "references/dml/entity/types/dml.entity.DMLEntitySchemaBuilder/page.mdx": "2025-01-13T18:05:57.748Z", "references/fulfillment/interfaces/fulfillment.OrderCreditLineDTO/page.mdx": "2025-04-11T09:04:45.734Z", - "references/fulfillment_models/variables/fulfillment_models.Fulfillment/page.mdx": "2025-05-01T15:18:46.307Z", - "references/fulfillment_models/variables/fulfillment_models.FulfillmentAddress/page.mdx": "2025-05-01T15:18:46.289Z", - "references/fulfillment_models/variables/fulfillment_models.FulfillmentItem/page.mdx": "2025-05-01T15:18:46.293Z", - "references/fulfillment_models/variables/fulfillment_models.FulfillmentLabel/page.mdx": "2025-05-01T15:18:46.296Z", - "references/fulfillment_models/variables/fulfillment_models.FulfillmentProvider/page.mdx": "2025-05-01T15:18:46.297Z", - "references/fulfillment_models/variables/fulfillment_models.FulfillmentSet/page.mdx": "2025-05-01T15:18:46.299Z", - "references/fulfillment_models/variables/fulfillment_models.GeoZone/page.mdx": "2025-05-01T15:18:46.313Z", - "references/fulfillment_models/variables/fulfillment_models.ServiceZone/page.mdx": "2025-05-01T15:18:46.324Z", - "references/fulfillment_models/variables/fulfillment_models.ShippingOption/page.mdx": "2025-05-01T15:18:46.338Z", - "references/fulfillment_models/variables/fulfillment_models.ShippingOptionRule/page.mdx": "2025-05-01T15:18:46.327Z", - "references/fulfillment_models/variables/fulfillment_models.ShippingOptionType/page.mdx": "2025-05-01T15:18:46.330Z", - "references/fulfillment_models/variables/fulfillment_models.ShippingProfile/page.mdx": "2025-05-01T15:18:46.341Z", + "references/fulfillment_models/variables/fulfillment_models.Fulfillment/page.mdx": "2025-05-07T15:35:18.526Z", + "references/fulfillment_models/variables/fulfillment_models.FulfillmentAddress/page.mdx": "2025-05-07T15:35:18.508Z", + "references/fulfillment_models/variables/fulfillment_models.FulfillmentItem/page.mdx": "2025-05-07T15:35:18.512Z", + "references/fulfillment_models/variables/fulfillment_models.FulfillmentLabel/page.mdx": "2025-05-07T15:35:18.515Z", + "references/fulfillment_models/variables/fulfillment_models.FulfillmentProvider/page.mdx": "2025-05-07T15:35:18.516Z", + "references/fulfillment_models/variables/fulfillment_models.FulfillmentSet/page.mdx": "2025-05-07T15:35:18.519Z", + "references/fulfillment_models/variables/fulfillment_models.GeoZone/page.mdx": "2025-05-07T15:35:18.532Z", + "references/fulfillment_models/variables/fulfillment_models.ServiceZone/page.mdx": "2025-05-07T15:35:18.544Z", + "references/fulfillment_models/variables/fulfillment_models.ShippingOption/page.mdx": "2025-05-07T15:35:18.559Z", + "references/fulfillment_models/variables/fulfillment_models.ShippingOptionRule/page.mdx": "2025-05-07T15:35:18.547Z", + "references/fulfillment_models/variables/fulfillment_models.ShippingOptionType/page.mdx": "2025-05-07T15:35:18.551Z", + "references/fulfillment_models/variables/fulfillment_models.ShippingProfile/page.mdx": "2025-05-07T15:35:18.562Z", "references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/page.mdx": "2025-04-11T09:04:54.266Z", "references/order/interfaces/order.OrderCreditLineDTO/page.mdx": "2025-04-11T09:04:49.593Z", "references/types/HttpTypes/interfaces/types.HttpTypes.AdminFulfillmentProviderOption/page.mdx": "2024-12-23T12:30:28.566Z", @@ -5714,9 +5714,9 @@ export const generatedEditDates = { "app/commerce-modules/user/admin-widget-zones/page.mdx": "2024-12-24T08:48:14.186Z", "app/commerce-modules/currency/links-to-other-modules/page.mdx": "2025-04-17T15:40:23.148Z", "app/commerce-modules/customer/links-to-other-modules/page.mdx": "2025-04-17T15:43:54.168Z", - "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.refundPaymentsStep/page.mdx": "2025-05-01T15:18:39.299Z", + "references/core_flows/Payment/Steps_Payment/functions/core_flows.Payment.Steps_Payment.refundPaymentsStep/page.mdx": "2025-05-07T15:35:11.514Z", "references/core_flows/Payment/Steps_Payment/variables/core_flows.Payment.Steps_Payment.refundPaymentsStepId/page.mdx": "2025-01-07T12:54:17.422Z", - "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.refundPaymentsWorkflow/page.mdx": "2025-05-01T15:18:39.333Z", + "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.refundPaymentsWorkflow/page.mdx": "2025-05-07T15:35:11.545Z", "references/core_flows/Payment/Workflows_Payment/functions/core_flows.Payment.Workflows_Payment.validatePaymentsRefundStep/page.mdx": "2025-01-20T08:25:22.137Z", "references/core_flows/Payment/Workflows_Payment/variables/core_flows.Payment.Workflows_Payment.refundPaymentsWorkflowId/page.mdx": "2025-01-07T12:54:17.465Z", "references/core_flows/Tax/Steps_Tax/functions/core_flows.Tax.Steps_Tax.updateTaxRegionsStep/page.mdx": "2025-04-11T09:04:41.551Z", @@ -5772,7 +5772,7 @@ export const generatedEditDates = { "app/commerce-modules/tax/workflows/page.mdx": "2025-01-09T13:41:46.504Z", "app/commerce-modules/user/js-sdk/page.mdx": "2025-01-09T12:55:02.289Z", "app/commerce-modules/user/workflows/page.mdx": "2025-01-09T13:41:46.302Z", - "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.batchInventoryItemLevelsWorkflow/page.mdx": "2025-05-01T15:18:37.307Z", + "references/core_flows/Inventory/Workflows_Inventory/functions/core_flows.Inventory.Workflows_Inventory.batchInventoryItemLevelsWorkflow/page.mdx": "2025-05-07T15:35:09.603Z", "references/core_flows/Inventory/Workflows_Inventory/variables/core_flows.Inventory.Workflows_Inventory.batchInventoryItemLevelsWorkflowId/page.mdx": "2025-01-13T17:30:23.527Z", "references/core_flows/Product/Workflows_Product/functions/core_flows.Product.Workflows_Product.validateProductInputStep/page.mdx": "2025-02-24T10:48:32.070Z", "references/core_flows/interfaces/core_flows.BatchInventoryItemLevelsWorkflowInput/page.mdx": "2025-01-13T17:30:26.219Z", @@ -5827,13 +5827,13 @@ export const generatedEditDates = { "references/utils/PromotionUtils/enums/utils.PromotionUtils.PromotionStatus/page.mdx": "2025-01-17T16:43:30.762Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateAndReturnShippingMethodsDataStep/page.mdx": "2025-03-04T13:33:40.459Z", "references/core_flows/Cart/Steps_Cart/functions/core_flows.Cart.Steps_Cart.validateCartStep/page.mdx": "2025-04-11T09:04:35.708Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshCartItemsWorkflow/page.mdx": "2025-05-01T15:18:36.672Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshCartShippingMethodsWorkflow/page.mdx": "2025-05-01T15:18:36.675Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrUpdateOrderPaymentCollectionWorkflow/page.mdx": "2025-05-01T15:18:38.034Z", - "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.deleteRefundReasonsWorkflow/page.mdx": "2025-05-01T15:18:39.374Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshCartItemsWorkflow/page.mdx": "2025-05-07T15:35:08.855Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.refreshCartShippingMethodsWorkflow/page.mdx": "2025-05-07T15:35:08.858Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrUpdateOrderPaymentCollectionWorkflow/page.mdx": "2025-05-07T15:35:10.324Z", + "references/core_flows/Payment_Collection/Workflows_Payment_Collection/functions/core_flows.Payment_Collection.Workflows_Payment_Collection.deleteRefundReasonsWorkflow/page.mdx": "2025-05-07T15:35:11.584Z", "references/core_flows/Product/Steps_Product/functions/core_flows.Product.Steps_Product.getVariantAvailabilityStep/page.mdx": "2025-01-17T16:43:24.975Z", "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionsStatusWorkflow/page.mdx": "2025-05-01T15:18:39.777Z", - "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionsValidationStep/page.mdx": "2025-05-01T15:18:39.773Z", + "references/core_flows/Promotion/Workflows_Promotion/functions/core_flows.Promotion.Workflows_Promotion.updatePromotionsValidationStep/page.mdx": "2025-05-07T15:35:11.904Z", "references/core_flows/interfaces/core_flows.ValidateCartStepInput/page.mdx": "2025-04-11T09:04:41.757Z", "references/core_flows/interfaces/core_flows.ValidateProductInputStepInput/page.mdx": "2025-02-24T10:48:34.257Z", "references/core_flows/types/core_flows.ThrowUnlessPaymentCollectionNotePaidInput/page.mdx": "2025-01-17T16:43:25.819Z", @@ -5842,17 +5842,17 @@ export const generatedEditDates = { "app/plugins/guides/wishlist/page.mdx": "2025-04-17T08:48:07.059Z", "app/plugins/page.mdx": "2025-02-26T11:39:25.709Z", "app/admin-components/components/data-table/page.mdx": "2025-03-03T14:55:58.556Z", - "references/order_models/variables/order_models.Order/page.mdx": "2025-05-01T15:18:48.245Z", - "references/order_models/variables/order_models.OrderAddress/page.mdx": "2025-05-01T15:18:48.229Z", + "references/order_models/variables/order_models.Order/page.mdx": "2025-05-07T15:35:20.474Z", + "references/order_models/variables/order_models.OrderAddress/page.mdx": "2025-05-07T15:35:20.454Z", "references/order_models/variables/order_models.OrderChange/page.mdx": "2025-01-27T11:43:58.781Z", "references/order_models/variables/order_models.OrderChangeAction/page.mdx": "2025-01-27T11:43:58.780Z", "references/order_models/variables/order_models.OrderClaim/page.mdx": "2025-01-27T11:43:58.775Z", "references/order_models/variables/order_models.OrderClaimItem/page.mdx": "2025-01-27T11:43:58.774Z", "references/order_models/variables/order_models.OrderClaimItemImage/page.mdx": "2025-01-27T11:43:58.774Z", - "references/order_models/variables/order_models.OrderCreditLine/page.mdx": "2025-05-01T15:18:48.233Z", + "references/order_models/variables/order_models.OrderCreditLine/page.mdx": "2025-05-07T15:35:20.459Z", "references/order_models/variables/order_models.OrderExchange/page.mdx": "2025-01-27T11:43:58.778Z", "references/order_models/variables/order_models.OrderExchangeItem/page.mdx": "2025-01-27T11:43:58.777Z", - "references/order_models/variables/order_models.OrderItem/page.mdx": "2025-05-01T15:18:48.240Z", + "references/order_models/variables/order_models.OrderItem/page.mdx": "2025-05-07T15:35:20.469Z", "references/order_models/variables/order_models.OrderLineItem/page.mdx": "2025-01-27T11:43:58.780Z", "references/order_models/variables/order_models.OrderLineItemAdjustment/page.mdx": "2025-01-27T11:43:58.779Z", "references/order_models/variables/order_models.OrderLineItemTaxLine/page.mdx": "2025-01-27T11:43:58.779Z", @@ -5860,11 +5860,11 @@ export const generatedEditDates = { "references/order_models/variables/order_models.OrderShippingMethod/page.mdx": "2025-01-27T11:43:58.792Z", "references/order_models/variables/order_models.OrderShippingMethodAdjustment/page.mdx": "2025-01-27T11:43:58.791Z", "references/order_models/variables/order_models.OrderShippingMethodTaxLine/page.mdx": "2025-01-27T11:43:58.791Z", - "references/order_models/variables/order_models.OrderSummary/page.mdx": "2025-05-01T15:18:48.243Z", + "references/order_models/variables/order_models.OrderSummary/page.mdx": "2025-05-07T15:35:20.472Z", "references/order_models/variables/order_models.OrderTransaction/page.mdx": "2025-01-27T11:43:58.792Z", "references/order_models/variables/order_models.Return/page.mdx": "2025-01-27T11:43:58.790Z", "references/order_models/variables/order_models.ReturnItem/page.mdx": "2025-01-27T11:43:58.789Z", - "references/order_models/variables/order_models.ReturnReason/page.mdx": "2025-05-01T15:18:48.246Z", + "references/order_models/variables/order_models.ReturnReason/page.mdx": "2025-05-07T15:35:20.475Z", "references/payment/IPaymentModuleService/methods/payment.IPaymentModuleService.listAndCountPaymentMethods/page.mdx": "2025-04-11T09:04:50.994Z", "references/payment/IPaymentModuleService/methods/payment.IPaymentModuleService.listPaymentMethods/page.mdx": "2025-04-11T09:04:50.992Z", "references/payment/interfaces/payment.FilterablePaymentMethodProps/page.mdx": "2025-02-24T10:48:40.957Z", @@ -5899,7 +5899,7 @@ export const generatedEditDates = { "references/payment/types/payment.PaymentAccountHolderDTO/page.mdx": "2025-02-11T11:36:52.944Z", "references/payment/types/payment.PaymentAddressDTO/page.mdx": "2025-02-11T11:36:52.940Z", "references/payment/types/payment.PaymentCustomerDTO/page.mdx": "2025-02-11T11:36:52.942Z", - "references/payment_models/variables/payment_models.AccountHolder/page.mdx": "2025-05-01T15:18:48.250Z", + "references/payment_models/variables/payment_models.AccountHolder/page.mdx": "2025-05-07T15:35:20.480Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StoreCartAddPromotion/page.mdx": "2025-02-11T11:36:48.999Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StoreCartPromotion/page.mdx": "2025-02-11T11:36:48.986Z", "references/types/HttpTypes/interfaces/types.HttpTypes.StoreCartRemovePromotion/page.mdx": "2025-02-11T11:36:48.998Z", @@ -5970,7 +5970,7 @@ export const generatedEditDates = { "references/core_flows/types/core_flows.CreateFulfillmentValidateOrderStepInput/page.mdx": "2025-04-11T09:04:42.353Z", "references/core_flows/types/core_flows.CreateOrderEditShippingMethodValidationStepInput/page.mdx": "2025-04-11T09:04:42.634Z", "references/core_flows/types/core_flows.CreateReturnShippingMethodValidationStepInput/page.mdx": "2025-04-11T09:04:42.875Z", - "references/core_flows/types/core_flows.CreateShipmentValidateOrderStepInput/page.mdx": "2025-04-11T09:04:42.368Z", + "references/core_flows/types/core_flows.CreateShipmentValidateOrderStepInput/page.mdx": "2025-05-07T15:35:12.576Z", "references/core_flows/types/core_flows.DeclineTransferOrderRequestValidationStepInput/page.mdx": "2025-04-11T09:04:43.069Z", "references/core_flows/types/core_flows.DismissItemReturnRequestValidationStepInput/page.mdx": "2025-04-11T09:04:42.889Z", "references/core_flows/types/core_flows.ExchangeAddNewItemValidationStepInput/page.mdx": "2025-04-11T09:04:42.471Z", @@ -6009,9 +6009,9 @@ export const generatedEditDates = { "app/examples/guides/quote-management/page.mdx": "2025-04-17T08:50:17.061Z", "references/cart/interfaces/cart.CartCreditLineDTO/page.mdx": "2025-03-04T13:33:48.207Z", "references/cart/interfaces/cart.UpdateLineItemWithoutSelectorDTO/page.mdx": "2025-03-04T13:33:48.254Z", - "references/cart_models/variables/cart_models.CreditLine/page.mdx": "2025-05-01T15:18:46.179Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.createCartCreditLinesWorkflow/page.mdx": "2025-05-01T15:18:36.631Z", - "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.deleteCartCreditLinesWorkflow/page.mdx": "2025-05-01T15:18:36.661Z", + "references/cart_models/variables/cart_models.CreditLine/page.mdx": "2025-05-07T15:35:18.394Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.createCartCreditLinesWorkflow/page.mdx": "2025-05-07T15:35:08.767Z", + "references/core_flows/Cart/Workflows_Cart/functions/core_flows.Cart.Workflows_Cart.deleteCartCreditLinesWorkflow/page.mdx": "2025-05-07T15:35:08.843Z", "references/core_flows/Cart/Workflows_Cart/variables/core_flows.Cart.Workflows_Cart.createCartCreditLinesWorkflowId/page.mdx": "2025-03-04T13:33:40.515Z", "references/core_flows/Cart/Workflows_Cart/variables/core_flows.Cart.Workflows_Cart.deleteCartCreditLinesWorkflowId/page.mdx": "2025-03-04T13:33:40.574Z", "references/fulfillment/interfaces/fulfillment.CartCreditLineDTO/page.mdx": "2025-03-04T13:33:48.973Z", @@ -6144,22 +6144,22 @@ export const generatedEditDates = { "references/types/FileTypes/types/types.FileTypes.FileAccessPermission/page.mdx": "2025-04-23T07:49:49.087Z", "references/workflows/types/workflows.Void/page.mdx": "2025-04-23T07:49:56.553Z", "references/core_flows/Draft_Order/Steps_Draft_Order/functions/core_flows.Draft_Order.Steps_Draft_Order.validateDraftOrderStep/page.mdx": "2025-04-24T08:23:52.495Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderItemsWorkflow/page.mdx": "2025-04-24T08:23:52.526Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderPromotionWorkflow/page.mdx": "2025-04-24T08:23:52.561Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderShippingMethodsWorkflow/page.mdx": "2025-04-24T08:23:52.591Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.beginDraftOrderEditWorkflow/page.mdx": "2025-04-24T08:23:52.627Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.cancelDraftOrderEditWorkflow/page.mdx": "2025-04-24T08:23:52.634Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.confirmDraftOrderEditWorkflow/page.mdx": "2025-04-24T08:23:52.659Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.convertDraftOrderWorkflow/page.mdx": "2025-04-24T08:23:52.701Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderActionItemWorkflow/page.mdx": "2025-04-24T08:23:52.732Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderActionShippingMethodWorkflow/page.mdx": "2025-04-24T08:23:52.762Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderPromotionsWorkflow/page.mdx": "2025-04-24T08:23:52.790Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderShippingMethodWorkflow/page.mdx": "2025-04-24T08:23:52.983Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.requestDraftOrderEditWorkflow/page.mdx": "2025-04-24T08:23:52.816Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderActionItemWorkflow/page.mdx": "2025-04-24T08:23:52.842Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderActionShippingMethodWorkflow/page.mdx": "2025-04-24T08:23:52.871Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderItemWorkflow/page.mdx": "2025-04-24T08:23:52.895Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderShippingMethodWorkflow/page.mdx": "2025-04-24T08:23:52.958Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderItemsWorkflow/page.mdx": "2025-05-07T15:35:09.030Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderPromotionWorkflow/page.mdx": "2025-05-07T15:35:09.046Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderShippingMethodsWorkflow/page.mdx": "2025-05-07T15:35:09.063Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.beginDraftOrderEditWorkflow/page.mdx": "2025-05-07T15:35:09.079Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.cancelDraftOrderEditWorkflow/page.mdx": "2025-05-07T15:35:09.081Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.confirmDraftOrderEditWorkflow/page.mdx": "2025-05-07T15:35:09.097Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.convertDraftOrderWorkflow/page.mdx": "2025-05-07T15:35:09.130Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderActionItemWorkflow/page.mdx": "2025-05-07T15:35:09.178Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderActionShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:09.194Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderPromotionsWorkflow/page.mdx": "2025-05-07T15:35:09.211Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.removeDraftOrderShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:09.378Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.requestDraftOrderEditWorkflow/page.mdx": "2025-05-07T15:35:09.228Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderActionItemWorkflow/page.mdx": "2025-05-07T15:35:09.245Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderActionShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:09.262Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderItemWorkflow/page.mdx": "2025-05-07T15:35:09.279Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderShippingMethodWorkflow/page.mdx": "2025-05-07T15:35:09.319Z", "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderWorkflow/page.mdx": "2025-04-24T08:23:53.010Z", "references/core_flows/Draft_Order/Workflows_Draft_Order/variables/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderItemsWorkflowId/page.mdx": "2025-04-23T16:21:18.492Z", "references/core_flows/Draft_Order/Workflows_Draft_Order/variables/core_flows.Draft_Order.Workflows_Draft_Order.addDraftOrderPromotionWorkflowId/page.mdx": "2025-04-23T16:21:18.508Z", @@ -6181,8 +6181,8 @@ export const generatedEditDates = { "references/core_flows/Draft_Order/core_flows.Draft_Order.Workflows_Draft_Order/page.mdx": "2025-04-24T08:23:52.496Z", "references/core_flows/Order/Steps_Order/functions/core_flows.Order.Steps_Order.registerOrderDeliveryStep/page.mdx": "2025-04-23T16:21:19.273Z", "references/core_flows/Order/Steps_Order/variables/core_flows.Order.Steps_Order.registerOrderDeliveryStepId/page.mdx": "2025-04-23T16:21:19.272Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderCreditLinesWorkflow/page.mdx": "2025-05-01T15:18:38.077Z", - "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.validateOrderCreditLinesStep/page.mdx": "2025-05-01T15:18:38.067Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.createOrderCreditLinesWorkflow/page.mdx": "2025-05-07T15:35:10.367Z", + "references/core_flows/Order/Workflows_Order/functions/core_flows.Order.Workflows_Order.validateOrderCreditLinesStep/page.mdx": "2025-05-07T15:35:10.358Z", "references/core_flows/Order/Workflows_Order/variables/core_flows.Order.Workflows_Order.createOrderCreditLinesWorkflowId/page.mdx": "2025-04-23T16:21:19.783Z", "references/core_flows/Order/functions/core_flows.Order.fetchShippingOptionForOrderWorkflow/page.mdx": "2025-05-01T15:18:39.228Z", "references/core_flows/Order/variables/core_flows.Order.fetchShippingOptionsForOrderWorkflowId/page.mdx": "2025-04-23T16:21:20.924Z", @@ -6195,7 +6195,7 @@ export const generatedEditDates = { "references/core_flows/types/core_flows.FetchShippingOptionForOrderWorkflowOutput/page.mdx": "2025-04-23T16:21:21.821Z", "references/core_flows/types/core_flows.RequestDraftOrderEditWorkflowInput/page.mdx": "2025-04-23T16:21:21.717Z", "references/core_flows/types/core_flows.GenerateProductCsvStepOutput/page.mdx": "2025-04-23T16:21:22.403Z", - "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.convertDraftOrderStep/page.mdx": "2025-05-01T15:18:36.914Z", + "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.convertDraftOrderStep/page.mdx": "2025-05-07T15:35:09.114Z", "references/core_flows/Draft_Order/Workflows_Draft_Order/functions/core_flows.Draft_Order.Workflows_Draft_Order.updateDraftOrderStep/page.mdx": "2025-04-24T08:23:52.930Z", "references/core_flows/interfaces/core_flows.AddDraftOrderPromotionWorkflowInput/page.mdx": "2025-04-24T08:23:57.277Z", "references/core_flows/interfaces/core_flows.AddDraftOrderShippingMethodsWorkflowInput/page.mdx": "2025-04-24T08:23:57.278Z", @@ -6208,16 +6208,42 @@ export const generatedEditDates = { "app/commerce-modules/product/guides/variant-inventory/page.mdx": "2025-04-25T14:22:42.329Z", "app/troubleshooting/validation-error/page.mdx": "2025-04-25T14:14:57.568Z", "app/integrations/guides/contentful/page.mdx": "2025-05-01T15:33:21.351Z", - "references/modules/events/page.mdx": "2025-05-01T15:18:45.921Z", - "references/module_events/module_events.Auth/page.mdx": "2025-05-01T15:18:45.932Z", - "references/module_events/module_events.Cart/page.mdx": "2025-05-01T15:18:45.930Z", - "references/module_events/module_events.Customer/page.mdx": "2025-05-01T15:18:45.931Z", - "references/module_events/module_events.Fulfillment/page.mdx": "2025-05-01T15:18:45.934Z", - "references/module_events/module_events.Order/page.mdx": "2025-05-01T15:18:45.931Z", - "references/module_events/module_events.Product/page.mdx": "2025-05-01T15:18:45.933Z", - "references/module_events/module_events.Region/page.mdx": "2025-05-01T15:18:45.934Z", - "references/module_events/module_events.Sales_Channel/page.mdx": "2025-05-01T15:18:45.933Z", - "references/module_events/module_events.User/page.mdx": "2025-05-01T15:18:45.932Z", + "references/modules/events/page.mdx": "2025-05-07T16:00:45.929Z", + "references/module_events/module_events.Auth/page.mdx": "2025-05-07T15:35:18.159Z", + "references/module_events/module_events.Cart/page.mdx": "2025-05-07T15:35:18.157Z", + "references/module_events/module_events.Customer/page.mdx": "2025-05-07T15:35:18.157Z", + "references/module_events/module_events.Fulfillment/page.mdx": "2025-05-07T15:35:18.161Z", + "references/module_events/module_events.Order/page.mdx": "2025-05-07T15:35:18.158Z", + "references/module_events/module_events.Product/page.mdx": "2025-05-07T15:35:18.160Z", + "references/module_events/module_events.Region/page.mdx": "2025-05-07T15:35:18.160Z", + "references/module_events/module_events.Sales_Channel/page.mdx": "2025-05-07T15:35:18.159Z", + "references/module_events/module_events.User/page.mdx": "2025-05-07T15:35:18.158Z", "references/modules/module_events/page.mdx": "2025-05-01T15:18:45.930Z", - "app/troubleshooting/medusa-admin/build-error/page.mdx": "2025-05-05T15:56:02.042Z" + "app/troubleshooting/medusa-admin/build-error/page.mdx": "2025-05-05T15:56:02.042Z", + "references/events/Auth/variables/events.Auth.AuthWorkflowEvents/page.mdx": "2025-05-07T15:35:18.138Z", + "references/events/Cart/variables/events.Cart.CartWorkflowEvents/page.mdx": "2025-05-07T15:35:18.131Z", + "references/events/Customer/variables/events.Customer.CustomerWorkflowEvents/page.mdx": "2025-05-07T15:35:18.132Z", + "references/events/Fulfillment/variables/events.Fulfillment.FulfillmentWorkflowEvents/page.mdx": "2025-05-07T15:35:18.147Z", + "references/events/Order/variables/events.Order.OrderEditWorkflowEvents/page.mdx": "2025-05-07T15:35:18.135Z", + "references/events/Order/variables/events.Order.OrderWorkflowEvents/page.mdx": "2025-05-07T15:35:18.134Z", + "references/events/Product/variables/events.Product.ProductCategoryWorkflowEvents/page.mdx": "2025-05-07T15:35:18.140Z", + "references/events/Product/variables/events.Product.ProductCollectionWorkflowEvents/page.mdx": "2025-05-07T15:35:18.141Z", + "references/events/Product/variables/events.Product.ProductOptionWorkflowEvents/page.mdx": "2025-05-07T15:35:18.145Z", + "references/events/Product/variables/events.Product.ProductTagWorkflowEvents/page.mdx": "2025-05-07T15:35:18.144Z", + "references/events/Product/variables/events.Product.ProductVariantWorkflowEvents/page.mdx": "2025-05-07T15:35:18.142Z", + "references/events/Product/variables/events.Product.ProductWorkflowEvents/page.mdx": "2025-05-07T15:35:18.142Z", + "references/events/Product/variables/events.Product.ProductTypeWorkflowEvents/page.mdx": "2025-05-07T15:35:18.143Z", + "references/events/Region/variables/events.Region.RegionWorkflowEvents/page.mdx": "2025-05-07T15:35:18.146Z", + "references/events/Sales_Channel/variables/events.Sales_Channel.SalesChannelWorkflowEvents/page.mdx": "2025-05-07T15:35:18.139Z", + "references/events/User/variables/events.User.InviteWorkflowEvents/page.mdx": "2025-05-07T15:35:18.137Z", + "references/events/User/variables/events.User.UserWorkflowEvents/page.mdx": "2025-05-07T15:35:18.136Z", + "references/events/events.Auth/page.mdx": "2025-05-07T15:35:18.137Z", + "references/events/events.Cart/page.mdx": "2025-05-07T15:35:18.130Z", + "references/events/events.Customer/page.mdx": "2025-05-07T15:35:18.131Z", + "references/events/events.Fulfillment/page.mdx": "2025-05-07T15:35:18.146Z", + "references/events/events.Order/page.mdx": "2025-05-07T15:35:18.133Z", + "references/events/events.Product/page.mdx": "2025-05-07T15:35:18.140Z", + "references/events/events.Region/page.mdx": "2025-05-07T15:35:18.145Z", + "references/events/events.Sales_Channel/page.mdx": "2025-05-07T15:35:18.138Z", + "references/events/events.User/page.mdx": "2025-05-07T15:35:18.135Z" } \ No newline at end of file diff --git a/www/apps/resources/package.json b/www/apps/resources/package.json index 4aefc1f541..fe0f8a9d9b 100644 --- a/www/apps/resources/package.json +++ b/www/apps/resources/package.json @@ -16,7 +16,7 @@ "dependencies": { "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@medusajs/icons": "~2.5.1", + "@medusajs/icons": "~2.7.1", "@next/mdx": "15.3.1", "clsx": "^2.1.0", "docs-ui": "*", diff --git a/www/apps/ui/package.json b/www/apps/ui/package.json index 8532090d3d..cda9b05714 100644 --- a/www/apps/ui/package.json +++ b/www/apps/ui/package.json @@ -16,9 +16,9 @@ "dependencies": { "@faker-js/faker": "^8.0.2", "@mdx-js/react": "^3.1.0", - "@medusajs/icons": "2.6.0", - "@medusajs/ui": "4.0.6", - "@medusajs/ui-preset": "2.6.0", + "@medusajs/icons": "2.7.1", + "@medusajs/ui": "4.0.9", + "@medusajs/ui-preset": "2.7.1", "autoprefixer": "10.4.14", "build-scripts": "*", "clsx": "^2.0.0", diff --git a/www/apps/user-guide/package.json b/www/apps/user-guide/package.json index d27f32362e..6769680fad 100644 --- a/www/apps/user-guide/package.json +++ b/www/apps/user-guide/package.json @@ -16,7 +16,7 @@ "dependencies": { "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", - "@medusajs/icons": "~2.5.1", + "@medusajs/icons": "~2.7.1", "@next/mdx": "15.0.4", "clsx": "^2.1.0", "docs-ui": "*", diff --git a/www/packages/docs-ui/package.json b/www/packages/docs-ui/package.json index 1be6f57f20..a7061e110b 100644 --- a/www/packages/docs-ui/package.json +++ b/www/packages/docs-ui/package.json @@ -57,8 +57,8 @@ }, "dependencies": { "@emotion/is-prop-valid": "^1.3.1", - "@medusajs/icons": "2.6.0", - "@medusajs/ui": "4.0.6", + "@medusajs/icons": "2.7.1", + "@medusajs/ui": "4.0.9", "@next/third-parties": "15.3.1", "@octokit/request": "^8.1.1", "@react-hook/resize-observer": "^1.2.6", diff --git a/www/packages/docs-ui/src/components/CopyButton/index.tsx b/www/packages/docs-ui/src/components/CopyButton/index.tsx index 6fd26c0caa..4a63bbc316 100644 --- a/www/packages/docs-ui/src/components/CopyButton/index.tsx +++ b/www/packages/docs-ui/src/components/CopyButton/index.tsx @@ -5,6 +5,10 @@ import clsx from "clsx" import { Tooltip } from "@/components" import { useCopy } from "../../hooks" +export type CopyButtonChildFn = (props: { + isCopied: boolean +}) => React.ReactNode + export type CopyButtonProps = { text: string buttonClassName?: string @@ -17,7 +21,8 @@ export type CopyButtonProps = { | React.TouchEvent ) => void handleTouch?: boolean -} & Omit, "onCopy"> + children?: React.ReactNode | CopyButtonChildFn +} & Omit, "onCopy" | "children"> export const CopyButton = ({ text, @@ -64,7 +69,7 @@ export const CopyButton = ({ } }} > - {children} + {typeof children === "function" ? children({ isCopied }) : children} ) diff --git a/www/packages/docs-ui/src/components/CopyGeneratedSnippetButton/index.tsx b/www/packages/docs-ui/src/components/CopyGeneratedSnippetButton/index.tsx new file mode 100644 index 0000000000..6774753cd8 --- /dev/null +++ b/www/packages/docs-ui/src/components/CopyGeneratedSnippetButton/index.tsx @@ -0,0 +1,31 @@ +"use client" + +import React from "react" +import { CopyButton, useGenerateSnippet, UseGenerateSnippet } from "../.." +import { SquareTwoStack, CheckCircle } from "@medusajs/icons" + +export type CopyGeneratedSnippetButtonProps = UseGenerateSnippet & { + tooltipText?: string +} + +export const CopyGeneratedSnippetButton = ({ + tooltipText, + ...props +}: CopyGeneratedSnippetButtonProps) => { + const { snippet } = useGenerateSnippet(props) + + return ( + + {({ isCopied }) => { + if (isCopied) { + return + } + return + }} + + ) +} diff --git a/www/packages/docs-ui/src/components/Heading/H1/index.tsx b/www/packages/docs-ui/src/components/Heading/H1/index.tsx index 2566b50994..91f420b257 100644 --- a/www/packages/docs-ui/src/components/Heading/H1/index.tsx +++ b/www/packages/docs-ui/src/components/Heading/H1/index.tsx @@ -2,7 +2,7 @@ import clsx from "clsx" import React from "react" import { LlmDropdown } from "../../LlmDropdown" -type H1Props = React.HTMLAttributes & { +export type H1Props = React.HTMLAttributes & { id?: string hideLlmDropdown?: boolean } diff --git a/www/packages/docs-ui/src/components/Heading/H2/index.tsx b/www/packages/docs-ui/src/components/Heading/H2/index.tsx index aa6fccc333..4aa87eddb6 100644 --- a/www/packages/docs-ui/src/components/Heading/H2/index.tsx +++ b/www/packages/docs-ui/src/components/Heading/H2/index.tsx @@ -5,7 +5,7 @@ import React from "react" import { CopyButton, Link } from "@/components" import { useHeadingUrl, useLayout } from "../../.." -type H2Props = React.HTMLAttributes & { +export type H2Props = React.HTMLAttributes & { id?: string passRef?: React.RefObject } diff --git a/www/packages/docs-ui/src/components/Heading/H3/index.tsx b/www/packages/docs-ui/src/components/Heading/H3/index.tsx index f78440ffb4..8ee4980e83 100644 --- a/www/packages/docs-ui/src/components/Heading/H3/index.tsx +++ b/www/packages/docs-ui/src/components/Heading/H3/index.tsx @@ -5,7 +5,7 @@ import React from "react" import { CopyButton, Link } from "@/components" import { useHeadingUrl, useLayout } from "../../.." -type H3Props = React.HTMLAttributes & { +export type H3Props = React.HTMLAttributes & { id?: string } diff --git a/www/packages/docs-ui/src/components/Heading/H4/index.tsx b/www/packages/docs-ui/src/components/Heading/H4/index.tsx index 68d104df37..393179b2e8 100644 --- a/www/packages/docs-ui/src/components/Heading/H4/index.tsx +++ b/www/packages/docs-ui/src/components/Heading/H4/index.tsx @@ -7,7 +7,7 @@ export const H4 = ({ }: React.HTMLAttributes) => { return (

) diff --git a/www/packages/docs-ui/src/components/LlmDropdown/index.tsx b/www/packages/docs-ui/src/components/LlmDropdown/index.tsx index eb5074157f..0c3c477e28 100644 --- a/www/packages/docs-ui/src/components/LlmDropdown/index.tsx +++ b/www/packages/docs-ui/src/components/LlmDropdown/index.tsx @@ -3,9 +3,8 @@ import React, { useRef, useState } from "react" import { useAiAssistant, useSiteConfig } from "../../providers" import { usePathname } from "next/navigation" -import { Button } from "../Button" import { AiAssistent, Book } from "@medusajs/icons" -import { Menu } from "../Menu" +import { DropdownMenu, Menu } from "../Menu" import { MarkdownIcon } from "../Icons/Markdown" import { useAiAssistantChat } from "../../providers/AiAssistant/Chat" import clsx from "clsx" @@ -30,43 +29,41 @@ export const LlmDropdown = () => { const pageUrl = `${baseUrl}${basePath}${pathname}` return ( -
- - , - openInNewTab: true, - }, - { - type: "action", - title: "Ask AI Assistant", - action: () => { - if (loading) { - return - } - setQuestion(`Explain the page ${pageUrl}`) - setChatOpened(true) - setOpen(false) + } + menuComponent={ + , + openInNewTab: true, }, - icon: , - }, - ]} - className={clsx( - "absolute right-0 top-[calc(100%+8px)] w-max", - !open && "hidden" - )} - /> -
+ { + type: "action", + title: "Ask AI Assistant", + action: () => { + if (loading) { + return + } + setQuestion(`Explain the page ${pageUrl}`) + setChatOpened(true) + setOpen(false) + }, + icon: , + }, + ]} + className={clsx( + "absolute right-0 top-[calc(100%+8px)] w-max", + !open && "hidden" + )} + /> + } + className="hidden md:block" + /> ) } diff --git a/www/packages/docs-ui/src/components/Menu/Dropdown/index.tsx b/www/packages/docs-ui/src/components/Menu/Dropdown/index.tsx new file mode 100644 index 0000000000..dd9e4a5a25 --- /dev/null +++ b/www/packages/docs-ui/src/components/Menu/Dropdown/index.tsx @@ -0,0 +1,76 @@ +"use client" + +import React from "react" +import { useRef, useState } from "react" +import { Button, Menu, useClickOutside } from "../../.." +import { MenuItem } from "types" +import clsx from "clsx" + +type DropdownMenuProps = { + dropdownButtonContent: React.ReactNode + dropdownButtonClassName?: string + menuComponent?: React.ReactNode + menuItems?: MenuItem[] + menuClassName?: string + className?: string + open?: boolean + setOpen?: (open: boolean) => void +} + +export const DropdownMenu = ({ + dropdownButtonContent, + dropdownButtonClassName, + menuComponent, + menuItems, + menuClassName, + className, + open: externalOpen = false, + setOpen: externalSetOpen, +}: DropdownMenuProps) => { + const [open, setOpen] = useState(externalOpen) + const ref = useRef(null) + function changeOpenState(newOpenState: boolean) { + if (externalSetOpen) { + externalSetOpen(newOpenState) + } else { + setOpen(newOpenState) + } + } + useClickOutside({ + elmRef: ref, + onClickOutside: () => { + changeOpenState(false) + }, + }) + + if (!menuComponent && !menuItems) { + return null + } + + return ( +
+ + {menuComponent} + {!menuComponent && menuItems && ( + + )} +
+ ) +} diff --git a/www/packages/docs-ui/src/components/Menu/index.tsx b/www/packages/docs-ui/src/components/Menu/index.tsx index 097599ba67..67d9b79ad1 100644 --- a/www/packages/docs-ui/src/components/Menu/index.tsx +++ b/www/packages/docs-ui/src/components/Menu/index.tsx @@ -39,3 +39,5 @@ export const Menu = ({ items, className, itemsOnClick }: MenuProps) => { ) } + +export * from "./Dropdown" diff --git a/www/packages/docs-ui/src/components/index.ts b/www/packages/docs-ui/src/components/index.ts index f9d9dd2a90..23c622afee 100644 --- a/www/packages/docs-ui/src/components/index.ts +++ b/www/packages/docs-ui/src/components/index.ts @@ -15,6 +15,7 @@ export * from "./CodeMdx" export * from "./CodeTabs" export * from "./CodeTabs/Item" export * from "./CopyButton" +export * from "./CopyGeneratedSnippetButton" export * from "./Details" export * from "./Details/Summary" export * from "./DetailsList" diff --git a/www/packages/docs-ui/src/hooks/index.ts b/www/packages/docs-ui/src/hooks/index.ts index 1850d913bc..eb757426df 100644 --- a/www/packages/docs-ui/src/hooks/index.ts +++ b/www/packages/docs-ui/src/hooks/index.ts @@ -5,6 +5,7 @@ export * from "./use-click-outside" export * from "./use-collapsible" export * from "./use-collapsible-code-lines" export * from "./use-copy" +export * from "./use-generate-snippet" export * from "./use-heading-url" export * from "./use-current-learning-path" export * from "./use-is-external-link" diff --git a/www/packages/docs-ui/src/hooks/use-generate-snippet/index.tsx b/www/packages/docs-ui/src/hooks/use-generate-snippet/index.tsx new file mode 100644 index 0000000000..f6e92c09fc --- /dev/null +++ b/www/packages/docs-ui/src/hooks/use-generate-snippet/index.tsx @@ -0,0 +1,22 @@ +import { + subscriberSnippetGenerator, + SubscriberSnippetGeneratorOptions, +} from "./snippet-generators/subscriber" + +export type UseGenerateSnippet = { + type: "subscriber" + options: SubscriberSnippetGeneratorOptions +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const generators: Record string> = { + subscriber: subscriberSnippetGenerator, +} + +export const useGenerateSnippet = ({ type, options }: UseGenerateSnippet) => { + const snippet = generators[type](options) + + return { + snippet, + } +} diff --git a/www/packages/docs-ui/src/hooks/use-generate-snippet/snippet-generators/subscriber.ts b/www/packages/docs-ui/src/hooks/use-generate-snippet/snippet-generators/subscriber.ts new file mode 100644 index 0000000000..176e0993ce --- /dev/null +++ b/www/packages/docs-ui/src/hooks/use-generate-snippet/snippet-generators/subscriber.ts @@ -0,0 +1,65 @@ +import { parseEventPayload } from "../../../utils" + +export type SubscriberSnippetGeneratorOptions = { + event: string + payload: Record | string +} + +export const subscriberSnippetGenerator = ({ + event, + payload: initialPayload, +}: SubscriberSnippetGeneratorOptions) => { + const payload = + typeof initialPayload === "string" + ? parseEventPayload(initialPayload).payload_for_snippet + : initialPayload + // format subscriber name + const subscriberName = + event + .split(".") + .map((word) => + word.replace(/-./g, (match) => match.charAt(1).toUpperCase()) + ) + .map((word, index) => { + if (index === 0) { + return word.charAt(0).toLowerCase() + word.slice(1) + } + + return word.charAt(0).toUpperCase() + word.slice(1) + }) + .join("") + "Handler" + // format payload + const payloadType: Record = {} + Object.keys(payload).forEach((key) => { + const value = payload[key] + if (Array.isArray(value)) { + payloadType[key] = `${typeof value[0]}[]` + } else { + payloadType[key] = typeof value + } + }) + + const payloadString = JSON.stringify(payloadType, null, 2).replaceAll( + /"/g, + "" + ) + + // return the snippet + return subscriberSnippet + .replace("{{subscriberName}}", subscriberName) + .replace("{{event}}", event) + .replace("{{payload}}", payloadString) +} + +const subscriberSnippet = `import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" + +export default async function {{subscriberName}}({ + event: { data }, + container, +}: SubscriberArgs<{{payload}}>) { + // TODO handle event +} + +export const config: SubscriberConfig = { + event: "{{event}}", +}` diff --git a/www/packages/docs-ui/src/utils/event-parser.ts b/www/packages/docs-ui/src/utils/event-parser.ts new file mode 100644 index 0000000000..09eef3a299 --- /dev/null +++ b/www/packages/docs-ui/src/utils/event-parser.ts @@ -0,0 +1,54 @@ +import { OpenAPI } from "types" + +export function parseEventPayload(payloadStr: string) { + const payloadParams = payloadStr.matchAll(/([\w_]+),? \/\/ (\(\w*\) )*(.*)/g) + const payloadForSnippet: Record = {} + const payload = Array.from(payloadParams).map((match) => { + const name = match[1] + const type = (match[2]?.replace(/\(|\)/g, "") || "string").trim() + const description = match[3] + + if (type === "string") { + payloadForSnippet[name] = "test" + } else if (type === "number") { + payloadForSnippet[name] = 1 + } else if (type === "boolean") { + payloadForSnippet[name] = true + } else if (type === "object") { + payloadForSnippet[name] = {} + } else if (type === "array") { + payloadForSnippet[name] = [{}] + } + + return { + name, + type, + description, + } + }) + return { + parsed_payload: { + type: "object", + required: ["payload"], + properties: { + payload: { + type: "object", + description: "The payload emitted with the event", + required: [...payload.map((param) => param.name)], + properties: payload.reduce( + (acc, curr) => { + acc[curr.name] = { + type: curr.type as OpenAPI.OpenAPIV3.NonArraySchemaObjectType, + description: curr.description, + properties: {}, + } + return acc + }, + {} as Record + ), + }, + }, + } as OpenAPI.SchemaObject, + payload_for_snippet: payloadForSnippet, + } +} diff --git a/www/packages/docs-ui/src/utils/index.ts b/www/packages/docs-ui/src/utils/index.ts index a946dda6b8..aa160decac 100644 --- a/www/packages/docs-ui/src/utils/index.ts +++ b/www/packages/docs-ui/src/utils/index.ts @@ -3,6 +3,7 @@ export * from "./capitalize" export * from "./check-sidebar-item-visibility" export * from "./decode-str" export * from "./dom-utils" +export * from "./event-parser" export * from "./get-link-with-base-path" export * from "./get-local-search" export * from "./get-navbar-items" diff --git a/www/packages/docs-utils/src/estree-to-js.ts b/www/packages/docs-utils/src/estree-to-js.ts index 3726e29042..dcc66f47e2 100644 --- a/www/packages/docs-utils/src/estree-to-js.ts +++ b/www/packages/docs-utils/src/estree-to-js.ts @@ -5,6 +5,8 @@ import { Expression, ExpressionJsVar, ExpressionJsVarLiteral, + JSXElementExpression, + JSXTextExpression, LiteralExpression, ObjectExpression, } from "types" @@ -64,7 +66,23 @@ function expressionToJs( data: (expression as LiteralExpression).value, } as ExpressionJsVarLiteral case "JSXElement": - // ignore JSXElements - return + case "JSXFragment": + // Only take text children + let text = "" + ;(expression as JSXElementExpression).children.forEach((child) => { + if (child.type !== "JSXText") { + return + } + + text += (child as JSXTextExpression).value + }) + return { + original: { + type: "Literal", + value: text, + raw: `"${text}"`, + }, + data: text, + } } } diff --git a/www/packages/docs-utils/src/get-clean-md.ts b/www/packages/docs-utils/src/get-clean-md.ts index d3df31dacf..139d7ec835 100644 --- a/www/packages/docs-utils/src/get-clean-md.ts +++ b/www/packages/docs-utils/src/get-clean-md.ts @@ -14,6 +14,7 @@ import { parseComponentExample, parseComponentReference, parseDetails, + parseEventHeader, parseHookValues, parseIconSearch, parseNote, @@ -48,6 +49,7 @@ const parsers: Record = { HookValues: parseHookValues, Colors: parseColors, SplitList: parseSplitList, + EventHeader: parseEventHeader, } const isComponentAllowed = (nodeName: string): boolean => { diff --git a/www/packages/docs-utils/src/utils/parsers.ts b/www/packages/docs-utils/src/utils/parsers.ts index 59a67e3b5b..652da5a69a 100644 --- a/www/packages/docs-utils/src/utils/parsers.ts +++ b/www/packages/docs-utils/src/utils/parsers.ts @@ -951,6 +951,54 @@ export const parseSplitList: ComponentParser = ( return [SKIP, index] } +export const parseEventHeader: ComponentParser = ( + node: UnistNodeWithData, + index: number, + parent: UnistTree +): VisitorResult => { + const headerContent = node.attributes?.find( + (attr) => attr.name === "headerProps" + ) + const headerLvl = node.attributes?.find((attr) => attr.name === "headerLvl") + + if ( + !headerContent || + typeof headerContent.value === "string" || + !headerContent.value.data?.estree || + !headerLvl || + typeof headerLvl.value !== "string" || + !headerLvl.value + ) { + return + } + + const headerPropsJsVar = estreeToJs(headerContent.value.data.estree) + + if ( + !isExpressionJsVarObj(headerPropsJsVar) || + !("children" in headerPropsJsVar) || + !isExpressionJsVarLiteral(headerPropsJsVar.children) + ) { + return + } + + const headerLevel = parseInt(headerLvl.value, 10) + const headerChildren = headerPropsJsVar.children.data as string + + const headerChildrenNode = { + type: "text", + value: headerChildren, + } + const headerNode = { + type: "heading", + depth: headerLevel, + children: [headerChildrenNode], + } + + parent?.children.splice(index, 1, headerNode) + return [SKIP, index] +} + /** * Helpers */ @@ -964,11 +1012,13 @@ const getTextNode = (node: UnistNode): UnistNode | undefined => { textNode = child } else if (child.type === "paragraph") { textNode = getTextNode(child) + } else if (child.type === "link") { + textNode = getTextNode(child) } else if (child.value) { textNode = child } - return false + return textNode !== undefined }) return textNode diff --git a/www/packages/types/package.json b/www/packages/types/package.json index 356e6b1d06..e9d4b7234f 100644 --- a/www/packages/types/package.json +++ b/www/packages/types/package.json @@ -32,7 +32,7 @@ "watch": "tsc --watch" }, "devDependencies": { - "@medusajs/icons": "2.6.0", + "@medusajs/icons": "2.7.1", "@types/node": "^20.11.20", "rimraf": "^5.0.5", "tsconfig": "*", diff --git a/www/packages/types/src/remark-rehype.ts b/www/packages/types/src/remark-rehype.ts index 80153d2868..17d537e11f 100644 --- a/www/packages/types/src/remark-rehype.ts +++ b/www/packages/types/src/remark-rehype.ts @@ -37,6 +37,17 @@ export type LiteralExpression = { raw: string } +export type JSXElementExpression = { + type: "JSXElement" | "JSXFragment" + children: Expression[] +} + +export type JSXTextExpression = { + type: "JSXText" + value: string + raw: string +} + export type Expression = | { type: string @@ -44,6 +55,8 @@ export type Expression = | ArrayExpression | ObjectExpression | LiteralExpression + | JSXElementExpression + | JSXTextExpression export interface Estree { body?: { diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/events-listing.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/events-listing.ts index af46f8739a..bebf2d6f93 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/events-listing.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/events-listing.ts @@ -67,7 +67,7 @@ function formatEventsType( eventVariable.name.replaceAll("WorkflowEvents", "") ) if (showHeader) { - content.push(`## ${header} Events`) + content.push(`${"#".repeat(subtitleLevel - 1)} ${header} Events`) } content.push("") @@ -93,9 +93,7 @@ function formatEventsType( .find((tag) => tag.tag === "@eventName") ?.content.map((content) => content.text) .join("") || "" - eventName = `[${eventName}](#${slugify(eventName.replace(".", ""), { - lower: true, - })})` + eventName = `[${eventName}](#${getEventNameSlug(eventName)})` const eventDescription = event.comment?.summary .map((content) => content.text) .join("") @@ -149,32 +147,30 @@ function formatEventsType( const deprecatedTag = event.comment?.blockTags.find( (tag) => tag.tag === "@deprecated" ) + const deprecatedMessage = deprecatedTag?.content + .map((content) => content.text) + .join("") + .trim() - content.push(`${subHeaderPrefix} \`${eventName}\``) + content.push( + getEventHeading({ + titleLevel: subtitleLevel, + eventName: eventName || "", + payload: eventPayload || "", + deprecated: !!deprecatedTag, + deprecatedMessage, + }) + ) content.push("") - if (deprecatedTag) { - const deprecationText = deprecatedTag.content - .map((content) => content.text) - .join("") - .trim() - if (deprecationText.length) { - content.push(``) - } - - content.push(`Deprecated`) - - if (deprecationText.length) { - content.push(``) - } - content.push("") - } content.push(eventDescription || "") content.push("") content.push(`${subHeaderPrefix}# Payload`) content.push("") content.push(eventPayload || "") content.push("") - content.push(`${subHeaderPrefix}# Workflows Emitting this Event`) + content.push( + `${subHeaderPrefix}# Workflows Emitting this Event\n\nThe following workflows emit this event when they're executed. These workflows are executed by Medusa's API routes. You can also view the events emitted by API routes in the [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references.` + ) content.push("") workflows?.forEach((workflow) => { content.push(`- [${workflow}](/references/medusa-workflows/${workflow})`) @@ -188,3 +184,42 @@ function formatEventsType( return content.join("\n") } + +function getEventHeading({ + titleLevel, + eventName, + payload, + deprecated = false, + deprecatedMessage, +}: { + titleLevel: number + eventName: string + payload: string + deprecated?: boolean + deprecatedMessage?: string +}) { + const heading = [eventName] + if (deprecated) { + if (deprecatedMessage?.length) { + heading.push(``) + } + + heading.push(`Deprecated`) + + if (deprecatedMessage?.length) { + heading.push(``) + } + } + return `${heading.join( + "\n" + )}), className: "flex flex-wrap justify-center gap-docs_0.25" }} eventName="${eventName}" payload={\`${payload.replaceAll( + "`", + "\\`" + )}\`} />` +} + +function getEventNameSlug(eventName: string) { + return slugify(eventName.replace(".", ""), { lower: true }) +} diff --git a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-events.ts b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-events.ts index 6d9e742bb0..a358a437b2 100644 --- a/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-events.ts +++ b/www/utils/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/workflow-events.ts @@ -25,6 +25,7 @@ export default function () { str += ` \nEvent\n\n` str += ` \nDescription\n\n` str += ` \nPayload\n\n` + str += ` \nAction\n\n` str += ` \n` str += ` \n` str += ` \n` @@ -33,29 +34,35 @@ export default function () { .map((c) => c.text) .join(" ") .split("--") - let eventName = `\`${commentContent[0].trim()}\`` + const eventName = commentContent[0].trim() + const eventPayload = commentContent[2]?.trim() || "" + let eventNameFormatted = `\`${eventName}\`` const eventDescription = commentContent[1]?.trim() || "" - const eventPayload = (commentContent[2]?.trim() || "") + const eventPayloadFormatted = eventPayload .replace("```ts\n", "") .replace("\n```", "") const isDeprecated = commentContent.length >= 4 if (isDeprecated) { const deprecatedText = commentContent[4]?.trim() - eventName += `\n` + eventNameFormatted += `\n` if (deprecatedText) { - eventName += `` + eventNameFormatted += `` } - eventName += `Deprecated` + eventNameFormatted += `Deprecated` if (deprecatedText) { - eventName += `` + eventNameFormatted += `` } } str += ` \n` - str += ` \n${eventName}\n\n` + str += ` \n${eventNameFormatted}\n\n` str += ` \n${eventDescription}\n\n` - str += ` \n\`\`\`ts blockStyle="inline"\n${eventPayload}\n\`\`\`\n\n` + str += ` \n\`\`\`ts blockStyle="inline"\n${eventPayloadFormatted}\n\`\`\`\n\n` + str += ` \n\n\n` str += ` \n` }) str += ` \n` diff --git a/www/yarn.lock b/www/yarn.lock index 62f2e509bc..a063326ba6 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -1868,21 +1868,12 @@ __metadata: languageName: node linkType: hard -"@medusajs/icons@npm:2.6.0": - version: 2.6.0 - resolution: "@medusajs/icons@npm:2.6.0" +"@medusajs/icons@npm:2.7.1, @medusajs/icons@npm:~2.7.1": + version: 2.7.1 + resolution: "@medusajs/icons@npm:2.7.1" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - checksum: cb8628018398aba010bb7b1f653cb7dc91d9bd59a40f1e7ee4cdc7732559e75062db790fb609fd3f748a9b80ccc0f421267a414ed9f5a7a4e079ef643e65333e - languageName: node - linkType: hard - -"@medusajs/icons@npm:^2.5.1, @medusajs/icons@npm:~2.5.1": - version: 2.5.1 - resolution: "@medusajs/icons@npm:2.5.1" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - checksum: f7fbe17475d8c39bd584103ec662f90aa5c8f07159f5820648eba8a0adf42a0d48057518da5a22503e5d5eaca552cbb7b26792613e2ec84827be1bbbafb64201 + checksum: 6c271671b8ca5d912b14c8f6e91adcd9312e4290b13e9fabe2ad6eb2635051c769fadaa7f2157c90161d8a90f57fd5fb6f8b24b4f86024248414871478f0997c languageName: node linkType: hard @@ -1898,11 +1889,23 @@ __metadata: languageName: node linkType: hard -"@medusajs/ui@npm:4.0.6": - version: 4.0.6 - resolution: "@medusajs/ui@npm:4.0.6" +"@medusajs/ui-preset@npm:2.7.1": + version: 2.7.1 + resolution: "@medusajs/ui-preset@npm:2.7.1" dependencies: - "@medusajs/icons": ^2.5.1 + "@tailwindcss/forms": ^0.5.3 + tailwindcss-animate: ^1.0.6 + peerDependencies: + tailwindcss: ">=3.0.0" + checksum: 6b67c5a614e1a8f985cac256c7a363292ca3326ee02a067ec310fec4b3b24b36726e5bc34427b75efb3a395cba2c4e7ba7bd97acc2fb2bb25004570b979e63c0 + languageName: node + linkType: hard + +"@medusajs/ui@npm:4.0.9": + version: 4.0.9 + resolution: "@medusajs/ui@npm:4.0.9" + dependencies: + "@medusajs/icons": 2.7.1 "@tanstack/react-table": 8.20.5 clsx: ^1.2.1 copy-to-clipboard: ^3.3.3 @@ -1918,7 +1921,7 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - checksum: ff45b909b3573fd6b443eee0aac47a2f13dbd145ed161e0485f7e22e1e963345e2e38dba236d1b927715feaa79d3dc79671bdeb2075c6f83682dcf8827e6f962 + checksum: 17f265fa8075cd26f54cda0ef96ffc9afe853fdf10bd42d82ce830f64bc0b932e93f43a9863606a482875fc1f6214a8acb43fd2a6c4fc8f593fa6a4521fbae76 languageName: node linkType: hard @@ -6237,8 +6240,8 @@ __metadata: dependencies: "@mdx-js/loader": ^3.1.0 "@mdx-js/react": ^3.1.0 - "@medusajs/icons": 2.6.0 - "@medusajs/ui": 4.0.6 + "@medusajs/icons": 2.7.1 + "@medusajs/ui": 4.0.9 "@next/bundle-analyzer": 15.3.1 "@next/mdx": 15.3.1 "@react-hook/resize-observer": ^2.0.2 @@ -6617,7 +6620,7 @@ __metadata: dependencies: "@mdx-js/loader": ^3.1.0 "@mdx-js/react": ^3.1.0 - "@medusajs/icons": ~2.5.1 + "@medusajs/icons": ~2.7.1 "@next/mdx": 15.3.1 "@types/mdx": ^2.0.13 "@types/node": ^20 @@ -7979,8 +7982,8 @@ __metadata: resolution: "docs-ui@workspace:packages/docs-ui" dependencies: "@emotion/is-prop-valid": ^1.3.1 - "@medusajs/icons": 2.6.0 - "@medusajs/ui": 4.0.6 + "@medusajs/icons": 2.7.1 + "@medusajs/ui": 4.0.9 "@next/third-parties": 15.3.1 "@octokit/request": ^8.1.1 "@react-hook/resize-observer": ^1.2.6 @@ -15011,7 +15014,7 @@ __metadata: dependencies: "@mdx-js/loader": ^3.1.0 "@mdx-js/react": ^3.1.0 - "@medusajs/icons": ~2.5.1 + "@medusajs/icons": ~2.7.1 "@next/bundle-analyzer": ^15.1.1 "@next/mdx": 15.3.1 "@types/mdx": ^2.0.13 @@ -16737,7 +16740,7 @@ turbo@latest: version: 0.0.0-use.local resolution: "types@workspace:packages/types" dependencies: - "@medusajs/icons": 2.6.0 + "@medusajs/icons": 2.7.1 "@types/node": ^20.11.20 openapi-types: ^12.1.3 rimraf: ^5.0.5 @@ -16826,9 +16829,9 @@ turbo@latest: dependencies: "@faker-js/faker": ^8.0.2 "@mdx-js/react": ^3.1.0 - "@medusajs/icons": 2.6.0 - "@medusajs/ui": 4.0.6 - "@medusajs/ui-preset": 2.6.0 + "@medusajs/icons": 2.7.1 + "@medusajs/ui": 4.0.9 + "@medusajs/ui-preset": 2.7.1 "@types/node": 20.4.9 "@types/react": "npm:types-react@rc" "@types/react-dom": "npm:types-react@rc" @@ -17220,7 +17223,7 @@ turbo@latest: dependencies: "@mdx-js/loader": ^3.1.0 "@mdx-js/react": ^3.1.0 - "@medusajs/icons": ~2.5.1 + "@medusajs/icons": ~2.7.1 "@next/mdx": 15.0.4 "@types/mdx": ^2.0.13 "@types/node": ^20