diff --git a/www/apps/book/middleware.ts b/www/apps/book/middleware.ts index 2dcad296b91cc..5d6b2c55673bc 100644 --- a/www/apps/book/middleware.ts +++ b/www/apps/book/middleware.ts @@ -2,12 +2,17 @@ import { NextResponse } from "next/server" import type { NextRequest } from "next/server" export function middleware(request: NextRequest) { - return NextResponse.rewrite( - new URL( - `/md-content${request.nextUrl.pathname.replace("/index.html.md", "")}`, - request.url + const path = request.nextUrl.pathname.replace("/index.html.md", "") + if ( + !path.startsWith("/resources") && + !path.startsWith("/ui") && + !path.startsWith("/api") && + !path.startsWith("/user-guide") + ) { + return NextResponse.rewrite( + new URL(`/md-content${path.replace("/index.html.md", "")}`, request.url) ) - ) + } } export const config = { diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index a7c0888e03f87..20f27cc014d21 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -43,7 +43,7 @@ This documentation is split into the following sections: 1. The main documentation, which is the one you're currently reading. It's recommended to follow the chapters in this documentation to understand the core concepts of Medusa and how to use them. 2. The [Development Resources documentation](https://docs.medusajs.com/resources/index.html.md) provides guides and resources useful during your development, such as tools, API references, recipes, step-by-step guides and examples, and more. -3. The [Store](https://docs.medusajs.com/api/store/index.html.md) and [Admin](https://docs.medusajs.com/api/admin/index.html.md) API references provide a reference to the Medusa application's endpoints and instructions related to authentication, parameter types in requests, and more. +3. The [Store](https://docs.medusajs.com/api/store) and [Admin](https://docs.medusajs.com/api/admin) API references provide a reference to the Medusa application's endpoints and instructions related to authentication, parameter types in requests, and more. To get started, check out the [Installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). @@ -140,6 +140,137 @@ By the end of this chapter, you’ll learn: - How to use Medusa’s `Logger` utility to log messages. +# Install Medusa + +In this chapter, you'll learn how to install and run a Medusa application. + +## Create Medusa Application + +A Medusa application is made up of a Node.js server and an admin dashboard. You can optionally install a separate [Next.js storefront](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md) either while installing the Medusa application or at a later point. + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +To create a Medusa application, use the `create-medusa-app` command: + +```bash +npx create-medusa-app@latest my-medusa-store +``` + +Where `my-medusa-store` is the name of the project's directory and PostgreSQL database created for the project. When you run the command, you'll be asked whether you want to install the Next.js storefront. + +After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to. + +If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`. + +### Successful Installation Result + +Once the installation finishes successfully, the Medusa application will run at `http://localhost:9000`. + +The Medusa Admin dashboard also runs at `http://localhost:9000/app`. The installation process opens the Medusa Admin dashboard in your default browser to create a user. You can later log in with that user. + +If you also installed the Next.js storefront, it'll be running at `http://localhost:8000`. + +You can stop the servers for the Medusa application and Next.js storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development). + +### Troubleshooting Installation Errors + +If you ran into an error during your installation, refer to the following troubleshooting guides for help: + +1. [create-medusa-app troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/create-medusa-app-errors/index.html.md). +2. [CORS errors](https://docs.medusajs.com/resources/troubleshooting/cors-errors/index.html.md). +3. [All troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md). + +If you can't find your error reported anywhere, please open a [GitHub issue](https://github.com/medusajs/medusa/issues/new/choose). + +*** + +## Run Medusa Application in Development + +To run the Medusa application in development, change to your application's directory and run the following command: + +```bash npm2yarn +npm run dev +``` + +This runs your Medusa application at `http://localhost:9000`, and the Medusa Admin dashboard `http://localhost:9000/app`. + +For details on starting and configuring the Next.js storefront, refer to [this documentation](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md). + +The application will restart if you make any changes to code under the `src` directory, except for admin customizations which are hot reloaded, providing you with a seamless developer experience without having to refresh your browser to see the changes. + +*** + +## Create Medusa Admin User + +Aside from creating an admin user in the admin dashboard, you can create a user with Medusa's CLI tool. + +Run the following command in your Medusa application's directory to create a new admin user: + +```bash +npx medusa user -e admin@medusajs.com -p supersecret +``` + +Replace `admin@medusajs.com` and `supersecret` with the user's email and password respectively. + +You can then use the user's credentials to log into the Medusa Admin application. + +*** + +## Project Files + +Your Medusa application's project will have the following files and directories: + +![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg) + +### src + +This directory is the central place for your custom development. It includes the following sub-directories: + +- `admin`: Holds your admin dashboard's custom [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) and [UI routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). +- `api`: Holds your custom [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are added as endpoints in your Medusa application. +- `jobs`: Holds your [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) that run at a specified interval during your Medusa application's runtime. +- `links`: Holds you [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that build associations between data models of different modules. +- `modules`: Holds your custom [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that implement custom business logic. +- `scripts`: Holds your custom [scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) to be executed using Medusa's CLI tool. +- `subscribers`: Holds your [event listeners](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) that are executed asynchronously whenever an event is emitted. +- `workflows`: Holds your custom [flows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that can be executed from anywhere in your application. + +### medusa-config.ts + +This file holds your [Medusa configurations](https://docs.medusajs.com/resources/references/medusa-config/index.html.md), such as your PostgreSQL database configurations. + +*** + +## Configure Medusa Application + +By default, your Medusa application is equipped with the basic configuration to start your development. + +If you run into issues with configurations, such as CORS configurations, or need to make changes to the default configuration, refer to [this guide on all available configurations](https://docs.medusajs.com/resources/references/medusa-config/index.html.md). + +*** + +## Update Medusa Application + +Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html.md) to learn how to update your Medusa project. + +*** + +## Next Steps + +In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. + + +# More Resources + +The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. + +Check out the Development Resources documentation [here](https://docs.medusajs.com/resources/index.html.md). + + # Medusa Deployment Overview In this chapter, you’ll learn the general approach to deploying the Medusa application. @@ -197,11 +328,26 @@ Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. -# More Resources +# Storefront Development -The Development Resources documentation provides guides and references that are useful for your development. This documentation included links to parts of the Development Resources documentation where necessary. +The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. -Check out the Development Resources documentation [here](https://docs.medusajs.com/resources/index.html.md). +You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. + +- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) +- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) + +*** + +## Passing a Publishable API Key in Storefront Requests + +When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. + +A publishable API key sets the scope of your request to one or more sales channels. + +Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. + +Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). # Updating Medusa @@ -310,277 +456,88 @@ npm run install ``` -# Storefront Development - -The Medusa application is made up of a Node.js server and an admin dashboard. Storefronts are installed, built, and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. - -You can build your storefront from scratch with your preferred tech stack, or start with our Next.js Starter storefront. The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. - -- [Install Next.js Starter Storefront](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) -- [Build Custom Storefront](https://docs.medusajs.com/resources/storefront-development/index.html.md) - -*** - -## Passing a Publishable API Key in Storefront Requests - -When sending a request to an API route starting with `/store`, you must include a publishable API key in the header of your request. - -A publishable API key sets the scope of your request to one or more sales channels. - -Then, when you retrieve products, only products of those sales channels are retrieved. This also ensures you retrieve correct inventory data, and associate created orders with the scoped sales channel. +# Using TypeScript Aliases -Learn more about passing the publishable API key in [this storefront development guide](https://docs.medusajs.com/resources/storefront-development/publishable-api-keys/index.html.md). +By default, Medusa doesn't support TypeScript aliases in production. +If you prefer using TypeScript aliases, install following development dependencies: -# Install Medusa +```bash npm2yarn +npm install --save-dev tsc-alias rimraf +``` -In this chapter, you'll learn how to install and run a Medusa application. +Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. -## Create Medusa Application +Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: -A Medusa application is made up of a Node.js server and an admin dashboard. You can optionally install a separate [Next.js storefront](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md) either while installing the Medusa application or at a later point. +```json title="package.json" +{ + "scripts": { + // other scripts... + "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", + "build": "npm run resolve:aliases && medusa build" + } +} +``` -### Prerequisites +You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) +```json title="tsconfig.json" +{ + "compilerOptions": { + // ... + "paths": { + "@/*": ["./src/*"] + } + } +} +``` -To create a Medusa application, use the `create-medusa-app` command: +Now, you can import modules, for example, using TypeScript aliases: -```bash -npx create-medusa-app@latest my-medusa-store +```ts +import { BrandModuleService } from "@/modules/brand/service" ``` -Where `my-medusa-store` is the name of the project's directory and PostgreSQL database created for the project. When you run the command, you'll be asked whether you want to install the Next.js storefront. - -After answering the prompts, the command installs the Medusa application in a directory with your project name, and sets up a PostgreSQL database that the application connects to. -If you chose to install the storefront with the Medusa application, the storefront is installed in a separate directory named `{project-name}-storefront`. +# Configure Instrumentation -### Successful Installation Result +In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. -Once the installation finishes successfully, the Medusa application will run at `http://localhost:9000`. +## Observability with OpenTelemtry -The Medusa Admin dashboard also runs at `http://localhost:9000/app`. The installation process opens the Medusa Admin dashboard in your default browser to create a user. You can later log in with that user. +Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: -If you also installed the Next.js storefront, it'll be running at `http://localhost:8000`. +- HTTP requests +- Workflow executions +- Query usages +- Database queries and operations -You can stop the servers for the Medusa application and Next.js storefront by exiting the installation command. To run the server for the Medusa application again, refer to [this section](#run-medusa-application-in-development). +*** -### Troubleshooting Installation Errors +## How to Configure Instrumentation in Medusa? -If you ran into an error during your installation, refer to the following troubleshooting guides for help: +### Prerequisites -1. [create-medusa-app troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/create-medusa-app-errors/index.html.md). -2. [CORS errors](https://docs.medusajs.com/resources/troubleshooting/cors-errors/index.html.md). -3. [All troubleshooting guides](https://docs.medusajs.com/resources/troubleshooting/index.html.md). +- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) -If you can't find your error reported anywhere, please open a [GitHub issue](https://github.com/medusajs/medusa/issues/new/choose). +### Install Dependencies -*** +Start by installing the following OpenTelemetry dependencies in your Medusa project: -## Run Medusa Application in Development +```bash npm2yarn +npm install @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/instrumentation-pg +``` -To run the Medusa application in development, change to your application's directory and run the following command: +Also, install the dependencies relevant for the exporter you use. If you're using Zipkin, install the following dependencies: ```bash npm2yarn -npm run dev +npm install @opentelemetry/exporter-zipkin ``` -This runs your Medusa application at `http://localhost:9000`, and the Medusa Admin dashboard `http://localhost:9000/app`. +### Add instrumentation.ts -For details on starting and configuring the Next.js storefront, refer to [this documentation](https://docs.medusajs.com/learn/storefront-development/nextjs-starter/index.html.md). - -The application will restart if you make any changes to code under the `src` directory, except for admin customizations which are hot reloaded, providing you with a seamless developer experience without having to refresh your browser to see the changes. - -*** - -## Create Medusa Admin User - -Aside from creating an admin user in the admin dashboard, you can create a user with Medusa's CLI tool. - -Run the following command in your Medusa application's directory to create a new admin user: - -```bash -npx medusa user -e admin@medusajs.com -p supersecret -``` - -Replace `admin@medusajs.com` and `supersecret` with the user's email and password respectively. - -You can then use the user's credentials to log into the Medusa Admin application. - -*** - -## Project Files - -Your Medusa application's project will have the following files and directories: - -![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg) - -### src - -This directory is the central place for your custom development. It includes the following sub-directories: - -- `admin`: Holds your admin dashboard's custom [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) and [UI routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). -- `api`: Holds your custom [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) that are added as endpoints in your Medusa application. -- `jobs`: Holds your [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) that run at a specified interval during your Medusa application's runtime. -- `links`: Holds you [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) that build associations between data models of different modules. -- `modules`: Holds your custom [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) that implement custom business logic. -- `scripts`: Holds your custom [scripts](https://docs.medusajs.com/learn/fundamentals/custom-cli-scripts/index.html.md) to be executed using Medusa's CLI tool. -- `subscribers`: Holds your [event listeners](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) that are executed asynchronously whenever an event is emitted. -- `workflows`: Holds your custom [flows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) that can be executed from anywhere in your application. - -### medusa-config.ts - -This file holds your [Medusa configurations](https://docs.medusajs.com/resources/references/medusa-config/index.html.md), such as your PostgreSQL database configurations. - -*** - -## Configure Medusa Application - -By default, your Medusa application is equipped with the basic configuration to start your development. - -If you run into issues with configurations, such as CORS configurations, or need to make changes to the default configuration, refer to [this guide on all available configurations](https://docs.medusajs.com/resources/references/medusa-config/index.html.md). - -*** - -## Update Medusa Application - -Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html.md) to learn how to update your Medusa project. - -*** - -## Next Steps - -In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. - - -# 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. - - -# 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. - -## Helpful Resources Guides - -The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: - -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 Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. - -Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn 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). - - -# 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. - -Medusa's 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. - - -# Configure Instrumentation - -In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. - -## Observability with OpenTelemtry - -Medusa uses [OpenTelemetry](https://opentelemetry.io/) for instrumentation and reporting. When configured, it reports traces for: - -- HTTP requests -- Workflow executions -- Query usages -- Database queries and operations - -*** - -## How to Configure Instrumentation in Medusa? - -### Prerequisites - -- [An exporter to visualize your application's traces, such as Zipkin.](https://zipkin.io/pages/quickstart.html) - -### 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: +Next, create the file `instrumentation.ts` with the following content: ```ts title="instrumentation.ts" import { registerOtel } from "@medusajs/medusa" @@ -793,27 +750,6 @@ The `activity` method returns the ID of the started activity. This ID can then b 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. -# 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. - - # Medusa Testing Tools In this chapter, you'll learn about Medusa's testing tools and how to install and configure them. @@ -910,389 +846,287 @@ Medusa's Testing Framework works for integration tests only. You can write unit The next chapters explain how to use the testing tools provided by `@medusajs/test-utils` to write tests. -# General Medusa Application Deployment Guide +# Build Custom Features -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. +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. -Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). +By following these guides, you'll add brands to the Medusa application that you can associate with products. -Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) +To build a custom feature in Medusa, you need three main tools: -Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. +- [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. -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: +![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. +*** -### Prerequisites +## Next Chapters: Brand Module Example -- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) +The next chapters will guide you to: -## Hosting Provider Requirements +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. -When you deploy your Medusa application, make sure your chosen hosting provider supports deploying the following resources: -1. PostgreSQL database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the PostgreSQL database. -2. Redis database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the Redis database. -3. Medusa application in server and worker mode. This means your hosting provider should support deploying two applications or instances from the same codebase. -4. For optimal experience, the hosting provider and plan must offer at least 2GB of RAM. +# 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). -## 1. Configure Medusa Application +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: -### Worker Mode +- 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). -The `workerMode` configuration determines which mode the Medusa application runs in. +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard -When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. +*** -Learn more about the `workerMode` configuration in [this document](https://docs.medusajs.com/resources/references/medusa-config#workermode/index.html.md). +## Next Chapters: View Brands in Dashboard -So, add the following configuration in `medusa-config.ts`: +In the next chapters, you'll continue with the brands example to: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", - }, -}) -``` +- 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. -Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. -### Configure Medusa Admin +# Extend Core Commerce Features -You need to disable the Medusa Admin in the worker Medusa application, while keeping it enabled in the server Medusa application. So, add the following configuration in `medusa-config.ts`: +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - disable: process.env.DISABLE_MEDUSA_ADMIN === "true", - }, -}) -``` +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. -Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable. +Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: -### Configure Redis URL +- [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. -The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. +*** -Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/resources/references/medusa-config#redisurl/index.html.md). +## Next Chapters: Link Brands to Products Example -So, add the following configuration in `medusa-config.ts` : +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: -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - redisUrl: process.env.REDIS_URL, - }, -}) -``` +- 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. -*** -## 2. Add predeploy Script +# Customizations Next Steps: Learn the Fundamentals -Before you start the Medusa application in production, you should always run migrations and sync links. +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. -So, add the following script in `package.json`: +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. -```json -"scripts": { - // ... - "predeploy": "medusa db:migrate" -}, -``` +## Helpful Resources Guides -*** +The [Development Resources](https://docs.medusajs.com/resources/index.html.md) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: -## 3. Install Production Modules and Providers +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. -By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. +*** -It’s highly recommended to instead use modules and providers suitable for production, including: +## More Examples in Recipes -- [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md) -- [Redis Event Bus Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) -- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) -- [S3 File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) (or other file module providers production-ready). -- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). +In the Development Resources documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. -Then, add these modules in `medusa-config.ts`: +Refer to the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation to learn more. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/event-bus-redis", - options: { - redisUrl: process.env.REDIS_URL, - }, - }, - { - resolve: "@medusajs/medusa/workflow-engine-redis", - options: { - redis: { - url: process.env.REDIS_URL, - }, - }, - }, - ], -}) -``` +# Medusa's Architecture -Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Architectural Modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) documentation for other modules and providers to use. +In this chapter, you'll learn about the architectural layers in Medusa. -*** +## HTTP, Workflow, and Module Layers -## 4. Create PostgreSQL and Redis Databases +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. -Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: -If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). +1. API Routes (HTTP): Our API Routes are the typical entry point. +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. -After hosting both databases, keep their connection URLs for the next steps. +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![Diagram illustrating the HTTP layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) *** -## 5. Deploy Medusa Application in Server Mode +## Database Layer -As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. +The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. -The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -### Set Environment Variables +![Diagram illustrating the database layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) -When setting the environment variables of the Medusa application, set the following variables: +*** -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -STORE_CORS= # STOREFRONT URL -ADMIN_CORS= # ADMIN URL -AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS -DISABLE_MEDUSA_ADMIN=false -MEDUSA_WORKER_MODE=server -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` +## Service Integrations -Where: +Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. -- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. -- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -Feel free to add any other relevant environment variables, such as for integrations and architectural modules. +### Commerce Modules -### Set Start Command +[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 integrate Stripe through a payment module provider. -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. +![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) -So, you must run the `start` command from the `.medusa/server` directory. +### Architectural Modules -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: +[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. -```bash npm2yarn -cd .medusa/server && npm run predeploy && npm run start -``` +![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) -### Set Backend URL in Admin Configuration +*** -After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: +## Full Diagram of Medusa's Architecture -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - // ... - backendUrl: process.env.MEDUSA_BACKEND_URL, - }, -}) -``` +The following diagram illustrates Medusa's architecture over the three layers. -Then, push the changes to the GitHub repository or deployed application. +![Full diagram illustrating Medusa's architecture](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) -In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: -```bash -ADMIN_CORS= # MEDUSA APPLICATION URL -AUTH_CORS= # ADD MEDUSA APPLICATION URL -MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION -``` +# Integrate Third-Party Systems -Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. +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. -Remember to separate URLs in `AUTH_CORS` by commas. +Medusa's 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: -## 6. Deploy Medusa Application in Worker Mode +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, you'll deploy the Medusa application in worker mode. +*** -As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. +## Next Chapters: Sync Brands Example -### Set Environment Variables +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: -When setting the environment variables of the Medusa application, set the following variables: +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. -```bash -COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET -JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET -DISABLE_MEDUSA_ADMIN=true -MEDUSA_WORKER_MODE=worker -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` -Where: +# Re-Use Customizations with Plugins -- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. -- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. -- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` -- Set the Redis database's connection URL as the value of `REDIS_URL`. +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. -Feel free to add any other relevant environment variables, such as for integrations and architectural modules. +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. -### Set Start Command +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. -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. +![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) -So, you must run the `start` command from the `.medusa/server` directory. +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). -```bash npm2yarn -cd .medusa/server && npm run predeploy && npm run start -``` -*** +# Admin Development -## 7. Test Deployed Application +In the next chapters, you'll learn more about possible admin customizations. -Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. +You can customize the admin dashboard by: -The Medusa Admin is also available at `/app`. +- Adding new sections to existing pages using Widgets. +- Adding new pages using UI Routes. *** -## Create Admin User - -If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: - -```bash -npx medusa user -e admin-medusa@test.com -p supersecret -``` - -Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. - -You can use these credentials to log into the Medusa Admin dashboard. - - -# Medusa's Architecture - -In this chapter, you'll learn about the architectural layers in Medusa. - -## HTTP, Workflow, and Module Layers +## Medusa UI Package -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. +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. -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: +Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components. -1. API Routes (HTTP): Our API Routes are the typical entry point. -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). +## Admin Components List -![Diagram illustrating the HTTP layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) +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. -*** -## Database Layer +# Custom CLI Scripts -The Medusa application injects into each module a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. +In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +## What is a Custom CLI Script? -![Diagram illustrating the database layer](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) +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. *** -## Service Integrations - -Third-party services are integrated through commerce and architectural modules. You also create custom third-party integrations through a custom module. +## How to Create a Custom CLI Script? -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +To create a custom CLI script, create a TypeScript or JavaScript file under the `src/scripts` directory. The file must default export a function. -### Commerce Modules +For example, create the file `src/scripts/my-script.ts` with the following content: -[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 integrate Stripe through a payment module provider. +```ts title="src/scripts/my-script.ts" +import { + ExecArgs, + IProductModuleService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" -![Diagram illustrating the commerce modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) +export default async function myScript({ container }: ExecArgs) { + const productModuleService: IProductModuleService = container.resolve( + Modules.PRODUCT + ) -### Architectural Modules + const [, count] = await productModuleService + .listAndCountProducts() -[Architectural modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) integrate third-party services and systems for architectural features. For example, you integrate Redis as a pub/sub service to send events, or SendGrid to send notifications. + console.log(`You have ${count} product(s)`) +} +``` -![Diagram illustrating the architectural modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) +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. *** -## Full Diagram of Medusa's Architecture - -The following diagram illustrates Medusa's architecture over the three layers. - -![Full diagram illustrating Medusa's architecture](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) - - -# Admin Development - -In the next chapters, you'll learn more about possible admin customizations. +## How to Run Custom CLI Script? -You can customize the admin dashboard by: +To run the custom CLI script, run the Medusa CLI's `exec` command: -- Adding new sections to existing pages using Widgets. -- Adding new pages using UI Routes. +```bash +npx medusa exec ./src/scripts/my-script.ts +``` *** -## Medusa UI Package +## Custom CLI Script Arguments -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. +Your script can accept arguments from the command line. Arguments are passed to the function's object parameter in the `args` property. -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) to learn how to install it and use its components. +For example: -*** +```ts +import { ExecArgs } from "@medusajs/framework/types" -## Admin Components List +export default async function myScript({ args }: ExecArgs) { + console.log(`The arguments you passed: ${args}`) +} +``` -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. +Then, pass the arguments in the `exec` command after the file path: + +```bash +npx medusa exec ./src/scripts/my-script.ts arg1 arg2 +``` # API Routes @@ -1354,89 +1188,6 @@ curl http://localhost:9000/hello-world 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 Advanced Guides - -Data models are created and managed in a module. To learn how to create a data model in a custom module, refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -In the next chapters, you'll learn about defining data models in more details. You'll learn about: - -- The different property types available. -- How to set a property as a primary key. -- How to create and manage relationships. -- How to configure properties, such as making them nullable or searchable. -- How to manually write migrations. - - # Environment Variables In this chapter, you'll learn how environment variables are loaded in Medusa. @@ -1481,49 +1232,199 @@ Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -# Events and Subscribers +# Medusa Container -In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. +In this chapter, you’ll learn about the Medusa container and how to use it. -## Handle Core Commerce Flows with Events +## What is the Medusa Container? -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. +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. -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. +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. -You listen to an event in a subscriber, which is an asynchronous function that's executed when its associated event is emitted. +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. -![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) +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: -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. +```ts highlights={highlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -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. +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") -### List of Emitted Events + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) -Find a list of all emitted events in [this reference](https://docs.medusajs.com/resources/events-reference/index.html.md). + 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). *** -## How to Create a Subscriber? +## List of Resources Registered in the Medusa Container -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. +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) -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) +## How to Resolve From the Medusa Container -```ts title="src/subscribers/product-created.ts" +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 { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -export default async function orderPlacedHandler({ +export default async function productCreateHandler({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const logger = container.resolve("logger") + const query = container.resolve(ContainerRegistrationKeys.QUERY) - logger.info("Sending confirmation email...") + 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). + + +# 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/events-reference/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({ @@ -1583,48 +1484,61 @@ Medusa provides two Event Modules out of the box: 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/architectural-modules/event/create/index.html.md). -# Using TypeScript Aliases +# Data Models Advanced Guides -By default, Medusa doesn't support TypeScript aliases in production. +Data models are created and managed in a module. To learn how to create a data model in a custom module, refer to the [Modules chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -If you prefer using TypeScript aliases, install following development dependencies: +In the next chapters, you'll learn about defining data models in more details. You'll learn about: -```bash npm2yarn -npm install --save-dev tsc-alias rimraf -``` +- The different property types available. +- How to set a property as a primary key. +- How to create and manage relationships. +- How to configure properties, such as making them nullable or searchable. +- How to manually write migrations. -Where `tsc-alias` is a package that resolves TypeScript aliases, and `rimraf` is a package that removes files and directories. -Then, add a new `resolve:aliases` script to your `package.json` and update the `build` script: +# Plugins -```json title="package.json" -{ - "scripts": { - // other scripts... - "resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json", - "build": "npm run resolve:aliases && medusa build" - } -} -``` +In this chapter, you'll learn what a plugin is in Medusa. -You can now use TypeScript aliases in your Medusa application. For example, add the following in `tsconfig.json`: +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -```json title="tsconfig.json" -{ - "compilerOptions": { - // ... - "paths": { - "@/*": ["./src/*"] - } - } -} -``` +## What is a Plugin? -Now, you can import modules, for example, using TypeScript aliases: +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). -```ts -import { BrandModuleService } from "@/modules/brand/service" -``` +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. + +*** + +## Plugin Guides and Resources + +For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/index.html.md). # Module Link @@ -1751,143 +1665,54 @@ export default defineLink( In this example, when a product is deleted, its linked `myCustom` record is also deleted. -# Extend Core Commerce Features +# Modules -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. +In this chapter, you’ll learn about modules and how to create them. -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. +## What is a Module? -Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: +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. -- [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. +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 implications on the existing setup. You can also re-use your modules across Medusa projects. -## Next Chapters: Link Brands to Products Example +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. -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: +- You want to build a custom feature related to a single domain or integrate a third-party service. -- 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. +- 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). +*** -# Next.js Starter Storefront +## How to Create a Module? -The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. +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. -The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. +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 expose an API endpoint to create a blog post. -In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/blog`. -## Install Next.js Starter +### 1. Create Data Model -### Prerequisites +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. -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) +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: -If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) -1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" -```bash -git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) + +export default Post ``` -2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: - -```bash npm2yarn -cd my-medusa-storefront -npm install -mv .env.template .env.local -``` - -3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys - -```bash -NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... -``` - -4. While the Medusa application is running, start the Next.js Starter storefront: - -```bash npm2yarn -npm run dev -``` - -Your Next.js Starter storefront is now running at `http://localhost:8000`. - -*** - -## Customize Storefront - -To customize the storefront, refer to the following directories: - -- `src/app`: The storefront’s pages. -- `src/modules`: The storefront’s components. -- `src/styles`: The storefront’s styles. - -You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). - -*** - -## Configurations and Integrations - -The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. - -Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. - - -# 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 implications on 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 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: +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`. @@ -2140,1516 +1965,1424 @@ This will create a post and return it in the response: 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. +# Scheduled Jobs -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +In this chapter, you’ll learn about scheduled jobs and how to use them. -## What is a Plugin? +## What is a Scheduled Job? -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). +When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. -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. +In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) +Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). +- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. +- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. +- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. *** -## 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 Scheduled Job? -*** +You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. -## How to Create a Plugin? +For example, create the file `src/jobs/hello-world.ts` with the following content: -The next chapter explains how you can create and publish a plugin. +![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) -*** +```ts title="src/jobs/hello-world.ts" highlights={highlights} +import { MedusaContainer } from "@medusajs/framework/types" -## Plugin Guides and Resources +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") -For more resources and guides related to plugins, refer to the [Resources documentation](https://docs.medusajs.com/resources/plugins/index.html.md). + logger.info("Greeting!") +} +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} +``` -# Workflows +You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. -In this chapter, you’ll learn about workflows and how to define and execute them. +You also export a `config` object that has the following properties: -## What is a Workflow? +- `name`: A unique name for the job. +- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. -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. +This scheduled job executes every minute and logs into the terminal `Greeting!`. -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. +### Test the Scheduled Job -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. +To test out your scheduled job, start the Medusa application: -However, unlike regular functions, workflows: +```bash npm2yarn +npm run dev +``` -- 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. +After a minute, the following message will be logged to the terminal: -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). +```bash +info: Greeting! +``` *** -## How to Create and Execute a Workflow? +## Example: Sync Products Once a Day -### 1. Create the Steps +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. -A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK. +When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. -Create the file `src/workflows/hello-world.ts` with the following content: +You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.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/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" -```ts title="src/workflows/hello-world.ts" highlights={step1Highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} -const step1 = createStep( - "step-1", - async () => { - return new StepResponse(`Hello from step one!`) - } -) +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} ``` -The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter. +In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. -Steps must return an instance of `StepResponse`, whose parameter is the data to return to the workflow executing the step. +The next time you start the Medusa application, it will run this job every day at midnight. -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 -} +# Next.js Starter Storefront -const step2 = createStep( - "step-2", - async ({ name }: WorkflowInput) => { - return new StepResponse(`Hello ${name} from step two!`) - } -) -``` +The Medusa application is made up of a Node.js server and an admin dashboard. The storefront is installed and hosted separately from the Medusa application, giving you the flexibility to choose the frontend tech stack that you and your team are proficient in, and implement unique design systems and user experience. -This adds another step whose function accepts as a parameter an object with a `name` property. +The Next.js Starter storefront provides rich commerce features and a sleek design. Developers and businesses can use it as-is or build on top of it to tailor it for the business's unique use case, design, and customer experience. -### 2. Create a Workflow +In this chapter, you’ll learn how to install the Next.js Starter storefront separately from the Medusa application. You can also install it while installing the Medusa application as explained in [the installation chapter](https://docs.medusajs.com/learn/installation/index.html.md). -Next, add the following to the same file to create the workflow using the `createWorkflow` function: +## Install Next.js Starter -```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights} -import { - // other imports... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +### Prerequisites -// ... +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1() - // to pass input - const str2 = step2(input) +If you already have a Medusa application installed with at least one region, you can install the Next.js Starter storefront with the following steps: - return new WorkflowResponse({ - message: str2, - }) - } -) +1. Clone the [Next.js Starter](https://github.com/medusajs/nextjs-starter-medusa): -export default myWorkflow +```bash +git clone https://github.com/medusajs/nextjs-starter-medusa my-medusa-storefront ``` -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. +2. Change to the `my-medusa-storefront` directory, install the dependencies, and rename the template environment variable file: -### 3. Execute the Workflow +```bash npm2yarn +cd my-medusa-storefront +npm install +mv .env.template .env.local +``` -You can execute a workflow from different customizations: +3. Set the Medusa application's publishable API key in the `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` environment variable. You can retrieve the publishable API key in on the Medusa Admin dashboard by going to Settings -> Publishable API Keys -- 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) -} +```bash +NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=pk_123... ``` -### 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) -} +4. While the Medusa application is running, start the Next.js Starter storefront: -export const config: SubscriberConfig = { - event: "order.placed", -} +```bash npm2yarn +npm run dev ``` -### 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" +Your Next.js Starter storefront is now running at `http://localhost:8000`. -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await myWorkflow(container) - .run({ - input: { - name: "John", - }, - }) +*** - console.log(result.message) -} +## Customize Storefront -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -}; -``` +To customize the storefront, refer to the following directories: -### 4. Test Workflow +- `src/app`: The storefront’s pages. +- `src/modules`: The storefront’s components. +- `src/styles`: The storefront’s styles. -To test out your workflow, start your Medusa application: +You can learn more about development with Next.js through [their documentation](https://nextjs.org/docs/getting-started). -```bash npm2yarn -npm run dev -``` +*** -Then, if you added the API route above, send a `GET` request to `/workflow`: +## Configurations and Integrations -```bash -curl http://localhost:9000/workflow -``` +The Next.js Starter is compatible with some Medusa integrations out-of-the-box, such as the Stripe provider module. You can also change some of its configurations if necessary. -You’ll receive the following response: +Refer to the [Next.js Starter reference](https://docs.medusajs.com/resources/nextjs-starter/index.html.md) for more details. -```json title="Example Response" -{ - "message": "Hello John from step two!" -} -``` -*** +# General Medusa Application Deployment Guide -## Access Medusa Container in Workflow Steps +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. -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. +Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). -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: +Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/contact) -```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" +Medusa Cloud is our managed services offering that makes deploying and operating Medusa applications possible without having to worry about configuring, scaling, and maintaining infrastructure. Medusa Cloud hosts your server, Admin dashboard, database, and Redis instance. -const getProductCountStep = createStep( - "get-product-count", - async (_, { container }) => { - const productModuleService = container.resolve("product") +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - const [, count] = await productModuleService.listAndCountProducts() +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. - return new StepResponse(count) - } -) +### Prerequisites -const productCountWorkflow = createWorkflow( - "product-count", - function () { - const count = getProductCountStep() +- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) - return new WorkflowResponse({ - count, - }) - } -) +## Hosting Provider Requirements -export default productCountWorkflow -``` +When you deploy your Medusa application, make sure your chosen hosting provider supports deploying the following resources: -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`. +1. PostgreSQL database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the PostgreSQL database. +2. Redis database: If your hosting provider doesn't support database hosting, you must find another hosting provider for the Redis database. +3. Medusa application in server and worker mode. This means your hosting provider should support deploying two applications or instances from the same codebase. +4. For optimal experience, the hosting provider and plan must offer at least 2GB of RAM. -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. +## 1. Configure Medusa Application +### Worker Mode -# Guide: Create Brand API Route +The `workerMode` configuration determines which mode the Medusa application runs in. -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. +When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. -An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. +Learn more about the `workerMode` configuration in [this document](https://docs.medusajs.com/resources/references/medusa-config#workermode/index.html.md). -The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin/index.html.md) and [store](https://docs.medusajs.com/api/store/index.html.md) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. +So, add the following configuration in `medusa-config.ts`: -### Prerequisites +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", + }, +}) +``` -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. -## 1. Create the API Route +### Configure Medusa Admin -You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). +You need to disable the Medusa Admin in the worker Medusa application, while keeping it enabled in the server Medusa application. So, add the following configuration in `medusa-config.ts`: -Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + disable: process.env.DISABLE_MEDUSA_ADMIN === "true", + }, +}) +``` -The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: +Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable. -![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) +### Configure Redis URL -```ts title="src/api/admin/brands/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createBrandWorkflow, -} from "../../../workflows/create-brand" +The `redisUrl` configuration specifies the connection URL to Redis to store the Medusa server's session. -type PostAdminCreateBrandType = { - name: string -} +Learn more in the [Medusa Configuration documentation](https://docs.medusajs.com/resources/references/medusa-config#redisurl/index.html.md). -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, - }) +So, add the following configuration in `medusa-config.ts` : - res.json({ brand: result }) -} +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + redisUrl: process.env.REDIS_URL, + }, +}) ``` -You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. +*** -The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds framework tools and custom and core modules' services. +## 2. Add predeploy Script -`MedusaRequest` accepts the request body's type as a type argument. +Before you start the Medusa application in production, you should always run migrations and sync links. -In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. +So, add the following script in `package.json`: -You return a JSON response with the created brand using the `res.json` method. +```json +"scripts": { + // ... + "predeploy": "medusa db:migrate" +}, +``` *** -## 2. Create Validation Schema - -The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. +## 3. Install Production Modules and Providers -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +By default, your Medusa application uses modules and providers useful for development, such as the In-Memory Cache Module or the Local File Module Provider. -Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). +It’s highly recommended to instead use modules and providers suitable for production, including: -You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: +- [Redis Cache Module](https://docs.medusajs.com/resources/architectural-modules/cache/redis/index.html.md) +- [Redis Event Bus Module](https://docs.medusajs.com/resources/architectural-modules/event/redis/index.html.md) +- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) +- [S3 File Module Provider](https://docs.medusajs.com/resources/architectural-modules/file/s3/index.html.md) (or other file module providers production-ready). +- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/architectural-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). -![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) +Then, add these modules in `medusa-config.ts`: -```ts title="src/api/admin/brands/validators.ts" -import { z } from "zod" +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -export const PostAdminCreateBrand = z.object({ - name: z.string(), +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/event-bus-redis", + options: { + redisUrl: process.env.REDIS_URL, + }, + }, + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.REDIS_URL, + }, + }, + }, + ], }) ``` -You export a validation schema that expects in the request body an object having a `name` property whose value is a string. - -You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: +Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Architectural Modules](https://docs.medusajs.com/resources/architectural-modules/index.html.md) documentation for other modules and providers to use. -```ts title="src/api/admin/brands/route.ts" -// ... -import { z } from "zod" -import { PostAdminCreateBrand } from "./validators" +*** -type PostAdminCreateBrandType = z.infer +## 4. Create PostgreSQL and Redis Databases -// ... -``` +Your Medusa application must connect to PostgreSQL and Redis databases. So, before you deploy it, create production PostgreSQL and Redis databases. -*** +If your hosting provider doesn't support databases, you can use [Neon for PostgreSQL database hosting](https://neon.tech/), and [Redis Cloud for the Redis database hosting](https://redis.io/cloud/). -## 3. Add Validation Middleware +After hosting both databases, keep their connection URLs for the next steps. -A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. +*** -Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). +## 5. Deploy Medusa Application in Server Mode -Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. +As mentioned earlier, you'll deploy two instances or create two deployments of your Medusa application: one in server mode, and the other in worker mode. -Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: +The deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. -![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) +### Set Environment Variables -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { PostAdminCreateBrand } from "./admin/brands/validators" +When setting the environment variables of the Medusa application, set the following variables: -export default defineMiddlewares({ - routes: [ - { - matcher: "/admin/brands", - method: "POST", - middlewares: [ - validateAndTransformBody(PostAdminCreateBrand), - ], - }, - ], -}) +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +STORE_CORS= # STOREFRONT URL +ADMIN_CORS= # ADMIN URL +AUTH_CORS= # STOREFRONT AND ADMIN URLS, SEPARATED BY COMMAS +DISABLE_MEDUSA_ADMIN=false +MEDUSA_WORKER_MODE=server +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL ``` -You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. +Where: -In the middleware object, you define three properties: +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- `STORE_CORS`'s value is the URL of your storefront. If you don’t have it yet, you can skip adding it for now. +- `ADMIN_CORS`'s value is the URL of the admin dashboard, which is the same as the server Medusa application. You can add it later if you don't currently have it. +- `AUTH_CORS`'s value is the URLs of any application authenticating users, customers, or other actor types, such as the storefront and admin URLs. The URLs are separated by commas. If you don’t have the URLs yet, you can set its value later. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `false` so that the admin is built with the server application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. -- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. -- `method`: The HTTP method to restrict the middleware to, which is `POST`. -- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. +Feel free to add any other relevant environment variables, such as for integrations and architectural modules. -The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. +### Set Start Command -*** +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. -## Test API Route +So, you must run the `start` command from the `.medusa/server` directory. -To test out the API route, start the Medusa application with the following command: +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: ```bash npm2yarn -npm run dev +cd .medusa/server && npm run predeploy && npm run start ``` -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. +### Set Backend URL in Admin Configuration -So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: +After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // ... + backendUrl: process.env.MEDUSA_BACKEND_URL, + }, +}) ``` -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, push the changes to the GitHub repository or deployed application. -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: +In your hosting provider, add or modify the following environment variables for the Medusa application in server mode: ```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' +ADMIN_CORS= # MEDUSA APPLICATION URL +AUTH_CORS= # ADD MEDUSA APPLICATION URL +MEDUSA_BACKEND_URL= # URL TO DEPLOYED MEDUSA APPLICATION ``` -This returns the created brand in the response: +Where you set the value of `ADMIN_CORS` and `MEDUSA_BACKEND_URL` to the Medusa application’s URL, and you add the URL to `AUTH_CORS`. -```json title="Example Response" -{ - "brand": { - "id": "01J7AX9ES4X113HKY6C681KDZJ", - "name": "Acme", - "created_at": "2024-09-09T08:09:34.244Z", - "updated_at": "2024-09-09T08:09:34.244Z" - } -} -``` +Remember to separate URLs in `AUTH_CORS` by commas. *** -## Summary - -By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: +## 6. Deploy Medusa Application in Worker Mode -1. Creating a module that defines and manages a `brand` table in the database. -2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. -3. Creating an API route that allows admin users to create a brand. +Next, you'll deploy the Medusa application in worker mode. -*** +As explained in the previous section, the deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. -## Next Steps: Associate Brand with Product +### Set Environment Variables -Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). +When setting the environment variables of the Medusa application, set the following variables: -In the next chapters, you'll learn how to build associations between data models defined in different modules. +```bash +COOKIE_SECRET=supersecret # TODO GENERATE SECURE SECRET +JWT_SECRET=supersecret # TODO GENERATE SECURE SECRET +DISABLE_MEDUSA_ADMIN=true +MEDUSA_WORKER_MODE=worker +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` +Where: -# Scheduled Jobs +- The value of `COOKIE_SECRET` and `JWT_SECRET` must be a randomly generated secret. +- Set `DISABLE_MEDUSA_ADMIN`'s value to `true` so that the admin isn't built with the worker application. +- Set the PostgreSQL database's connection URL as the value of `DATABASE_URL` +- Set the Redis database's connection URL as the value of `REDIS_URL`. -In this chapter, you’ll learn about scheduled jobs and how to use them. +Feel free to add any other relevant environment variables, such as for integrations and architectural modules. -## What is a Scheduled Job? +### Set Start Command -When building your commerce application, you may need to automate tasks and run them repeatedly at a specific schedule. For example, you need to automatically sync products to a third-party service once a day. +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. -In other commerce platforms, this feature isn't natively supported. Instead, you have to setup a separate application to execute cron jobs, which adds complexity as to how you expose this task to be executed in a cron job, or how do you debug it when it's not running within the platform's tooling. +So, you must run the `start` command from the `.medusa/server` directory. -Medusa removes this overhead by supporting this feature natively with scheduled jobs. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. Your efforts are only spent on implementing the functionality performed by the job, such as syncing products to an ERP. +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: -- You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. -- You want to execute the action once when the application loads. Use [loaders](https://docs.medusajs.com/learn/fundamentals/modules/loaders/index.html.md) instead. -- You want to execute the action if an event occurs. Use [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) instead. +```bash npm2yarn +cd .medusa/server && npm run predeploy && npm run start +``` *** -## How to Create a Scheduled Job? - -You create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. The file exports the asynchronous function to run, and the configurations indicating the schedule to run the function. +## 7. Test Deployed Application -For example, create the file `src/jobs/hello-world.ts` with the following content: +Once the application is deployed and live, go to `/health`, where `` is the URL of the Medusa application in server mode. If the deployment was successful, you’ll see the `OK` response. -![Example of scheduled job file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732866423/Medusa%20Book/scheduled-job-dir-overview_ediqgm.jpg) +The Medusa Admin is also available at `/app`. -```ts title="src/jobs/hello-world.ts" highlights={highlights} -import { MedusaContainer } from "@medusajs/framework/types" +*** -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") +## Create Admin User - logger.info("Greeting!") -} +If your hosting provider supports running commands in your Medusa application's directory, run the following command to create an admin user: -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} +```bash +npx medusa user -e admin-medusa@test.com -p supersecret ``` -You export an asynchronous function that receives the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) as a parameter. In the function, you resolve the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) from the Medusa container and log a message. +Replace the email `admin-medusa@test.com` and password `supersecret` with the credentials you want. -You also export a `config` object that has the following properties: +You can use these credentials to log into the Medusa Admin dashboard. -- `name`: A unique name for the job. -- `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. -This scheduled job executes every minute and logs into the terminal `Greeting!`. +# Workflows -### Test the Scheduled Job +In this chapter, you’ll learn about workflows and how to define and execute them. -To test out your scheduled job, start the Medusa application: +## What is a Workflow? -```bash npm2yarn -npm run dev -``` +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. -After a minute, the following message will be logged to the terminal: +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. -```bash -info: Greeting! -``` +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: -## Example: Sync Products Once a Day +- 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. -In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. +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). -When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. +*** -You can learn how to create a workflow in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), but this example assumes you already have a `syncProductToErpWorkflow` implemented. To execute this workflow once a day, create a scheduled job at `src/jobs/sync-products.ts` with the following content: +## How to Create and Execute a Workflow? -```ts title="src/jobs/sync-products.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" +### 1. Create the Steps -export default async function syncProductsJob(container: MedusaContainer) { - await syncProductToErpWorkflow(container) - .run() -} +A workflow is made of a series of steps. A step is created using `createStep` from the Workflows SDK. -export const config = { - name: "sync-products-job", - schedule: "0 0 * * *", -} +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!`) + } +) ``` -In the scheduled job function, you execute the `syncProductToErpWorkflow` by invoking it and passing it the container, then invoking the `run` method. You also specify in the exported configurations the schedule `0 0 * * *` which indicates midnight time of every day. +The `createStep` function accepts the step's unique name as a first parameter, and the step's function as a second parameter. -The next time you start the Medusa application, it will run this job every day at midnight. +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`: -# Guide: Create Brand Workflow +```ts title="src/workflows/hello-world.ts" highlights={step2Highlights} +type WorkflowInput = { + name: string +} -This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. +const step2 = createStep( + "step-2", + async ({ name }: WorkflowInput) => { + return new StepResponse(`Hello ${name} from step two!`) + } +) +``` -After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. +This adds another step whose function accepts as a parameter an object with a `name` property. -The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. +### 2. Create a Workflow -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +Next, add the following to the same file to create the workflow using the `createWorkflow` function: -### Prerequisites +```ts title="src/workflows/hello-world.ts" highlights={workflowHighlights} +import { + // other imports... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +// ... -*** +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1() + // to pass input + const str2 = step2(input) -## 1. Create createBrandStep + return new WorkflowResponse({ + message: str2, + }) + } +) -A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK +export default myWorkflow +``` -The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: +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. -![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) +The workflow must return an instance of `WorkflowResponse`, whose parameter is returned to workflow executors. -```ts title="src/workflows/create-brand.ts" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { BRAND_MODULE } from "../modules/brand" -import BrandModuleService from "../modules/brand/service" +### 3. Execute the Workflow -export type CreateBrandStepInput = { - name: string -} +You can execute a workflow from different customizations: -export const createBrandStep = createStep( - "create-brand-step", - async (input: CreateBrandStepInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +- 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. - const brand = await brandModuleService.createBrands(input) +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: - return new StepResponse(brand, brand.id) - } -) +### 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) +} ``` -You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. +### Subscriber -The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. +```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" -The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. +export default async function handleOrderPlaced({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) -So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. + console.log(result) +} -Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). +export const config: SubscriberConfig = { + event: "order.placed", +} +``` -A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. +### Scheduled Job -### Add Compensation Function to Step +```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" -You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await myWorkflow(container) + .run({ + input: { + name: "John", + }, + }) -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + console.log(result.message) +} -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +}; +``` -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +### 4. Test Workflow - await brandModuleService.deleteBrands(id) - } -) +To test out your workflow, start your Medusa application: + +```bash npm2yarn +npm run dev ``` -The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. +Then, if you added the API route above, send a `GET` request to `/workflow`: -In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. +```bash +curl http://localhost:9000/workflow +``` -Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). +You’ll receive the following response: -So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. +```json title="Example Response" +{ + "message": "Hello John from step two!" +} +``` *** -## 2. Create createBrandWorkflow +## Access Medusa Container in Workflow Steps -You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. +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. -Add the following content in the same `src/workflows/create-brand.ts` file: +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/create-brand.ts" -// other imports... +```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") -type CreateBrandWorkflowInput = { - name: string -} + const [, count] = await productModuleService.listAndCountProducts() -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) + return new StepResponse(count) + } +) - return new WorkflowResponse(brand) +const productCountWorkflow = createWorkflow( + "product-count", + function () { + const count = getProductCountStep() + + return new WorkflowResponse({ + count, + }) } ) + +export default productCountWorkflow ``` -You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. +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`. -The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. +You can now execute this workflow in a custom API route, scheduled job, or subscriber to get the total count of products. -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. +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. -*** -## Next Steps: Expose Create Brand API Route +# Write Integration Tests -You now have a `createBrandWorkflow` that you can execute to create a brand. +In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. -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. +### Prerequisites +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -# Medusa Container +## medusaIntegrationTestRunner Utility -In this chapter, you’ll learn about the Medusa container and how to use it. +The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. -## What is the Medusa Container? +For example: -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. +```ts title="integration-tests/http/test.spec.ts" highlights={highlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -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. +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + // TODO write tests... + }, +}) -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. +jest.setTimeout(60 * 1000) +``` -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: +The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. -```ts highlights={highlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") +- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: + - `get`: Send a `GET` request to an API route. + - `post`: Send a `POST` request to an API route. + - `delete`: Send a `DELETE` request to an API route. +- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). - res.json({ - products, - }) -} -``` +### Jest Timeout -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. +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: -You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +```ts title="integration-tests/http/test.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` *** -## 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) +### Run Tests -*** +Run the following command to run your tests: -## How to Resolve From the Medusa Container +```bash npm2yarn +npm run test:integration +``` -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. +If you don't have a `test:integration` 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). -### Subscriber +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. -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" +## Other Options and Inputs -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. - 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}`) -} +## Database Used in Tests -export const config: SubscriberConfig = { - event: `product.created`, -} -``` +The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -### Scheduled Job +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). -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" +## Example Integration Tests -export default async function myCustomJob( - container: MedusaContainer -) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) +The next chapters provide examples of writing integration tests for API routes and workflows. - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - console.log( - `You have ${products.length} matching your filters.` - ) -} +# Guide: Implement Brand Module -export const config = { - name: "every-minute-message", - // execute every minute - schedule: "* * * * *", -} -``` +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. -### Workflow Step +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. -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: +In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. -```ts highlights={workflowStepsHighlight} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -const step1 = createStep("step-1", async (_, { container }) => { - const query = container.resolve(ContainerRegistrationKeys.QUERY) +## 1. Create Module Directory - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) +Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. - return new StepResponse(products) -}) -``` +![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) -### 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. +## 2. Create Data Model -Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). +A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). -# Guide: Schedule Syncing Brands from CMS +You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content: -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. +![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) -However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. +```ts title="src/modules/brand/models/brand.ts" +import { model } from "@medusajs/framework/utils" -You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), +}) +``` -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +You create a `Brand` data model which has an `id` primary key property, and a `name` text property. -In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. +You define the data model using the `define` method of the DML. It accepts two parameters: -### Prerequisites +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. -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). *** -## 1. Implement Syncing Workflow +## 3. Create Module Service -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. +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. -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. +In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). -This workflow will have three steps: +You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: -1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. -2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. -3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) -### retrieveBrandsFromCmsStep +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" -To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: +class BrandModuleService extends MedusaService({ + Brand, +}) { -![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) +} -```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import CmsModuleService from "../modules/cms/service" -import { CMS_MODULE } from "../modules/cms" +export default BrandModuleService +``` -const retrieveBrandsFromCmsStep = createStep( - "retrieve-brands-from-cms", - async (_, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve( - CMS_MODULE - ) +The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. - const brands = await cmsModuleService.retrieveBrands() +The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. - return new StepResponse(brands) - } -) -``` +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). -You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). -### createBrandsStep +*** -The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: +## 4. Export Module Definition -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -// other imports... -import BrandModuleService from "../modules/brand/service" -import { BRAND_MODULE } from "../modules/brand" +A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. -// ... +So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: -type CreateBrand = { - name: string -} +![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) -type CreateBrandsInput = { - brands: CreateBrand[] -} +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" -export const createBrandsStep = createStep( - "create-brands-step", - async (input: CreateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +export const BRAND_MODULE = "brand" - const brands = await brandModuleService.createBrands(input.brands) +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` - return new StepResponse(brands, brands) - }, - async (brands, { container }) => { - if (!brands) { - return - } +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +1. The module's name (`brand`). You'll use this name when you use this module in other customizations. +2. An object with a required property `service` indicating the module's main service. - await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) - } -) -``` +You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. -The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. +*** -The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. +## 5. Add Module to Medusa's Configurations -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. -### Update Brands Step +The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: -The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/brand", + }, + ], +}) +``` -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} -// ... +The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). -type UpdateBrand = { - id: string - name: string -} +*** -type UpdateBrandsInput = { - brands: UpdateBrand[] -} +## 6. Generate and Run Migrations -export const updateBrandsStep = createStep( - "update-brands-step", - async ({ brands }: UpdateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. - const prevUpdatedBrands = await brandModuleService.listBrands({ - id: brands.map((brand) => brand.id), - }) +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). - const updatedBrands = await brandModuleService.updateBrands(brands) +[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: - return new StepResponse(updatedBrands, prevUpdatedBrands) - }, - async (prevUpdatedBrands, { container }) => { - if (!prevUpdatedBrands) { - return - } +```bash +npx medusa db:generate brand +npx medusa db:migrate +``` - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. - await brandModuleService.updateBrands(prevUpdatedBrands) - } -) -``` +*** -The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. +## Next Step: Create Brand Workflow -In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. -### Create Workflow +In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. -Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: -```ts title="src/workflows/sync-brands-from-cms.ts" -// other imports... -import { - // ... - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +# 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. -export const syncBrandsFromCmsWorkflow = createWorkflow( - "sync-brands-from-system", - () => { - const brands = retrieveBrandsFromCmsStep() +An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. - // TODO create and update brands - } -) -``` +The Medusa core application provides a set of [admin](https://docs.medusajs.com/api/admin) and [store](https://docs.medusajs.com/api/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. -In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. +### Prerequisites -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +## 1. Create the API Route -So, replace the `TODO` with the following: +You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). -```ts title="src/workflows/sync-brands-from-cms.ts" -const { toCreate, toUpdate } = transform( - { - brands, - }, - (data) => { - const toCreate: CreateBrand[] = [] - const toUpdate: UpdateBrand[] = [] +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). - data.brands.forEach((brand) => { - if (brand.external_id) { - toUpdate.push({ - id: brand.external_id as string, - name: brand.name as string, - }) - } else { - toCreate.push({ - name: brand.name as string, - }) - } - }) +The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: - return { toCreate, toUpdate } - } -) +![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) -// TODO create and update the brands -``` +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createBrandWorkflow, +} from "../../../workflows/create-brand" -`transform` accepts two parameters: +type PostAdminCreateBrandType = { + name: string +} -1. The data to be passed to the function in the second parameter. -2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) -In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. + res.json({ brand: result }) +} +``` -You now have the list of brands to create and update. So, replace the new `TODO` with the following: +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. -```ts title="src/workflows/sync-brands-from-cms.ts" -const created = createBrandsStep({ brands: toCreate }) -const updated = updateBrandsStep({ brands: toUpdate }) +The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) that holds framework tools and custom and core modules' services. -return new WorkflowResponse({ - created, - updated, -}) -``` +`MedusaRequest` accepts the request body's type as a type argument. -You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. +In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. -Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. +You return a JSON response with the created brand using the `res.json` method. *** -## 2. Schedule Syncing Task - -You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. +## 2. Create Validation Schema -A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: +The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. -![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -```ts title="src/jobs/sync-brands-from-cms.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" +Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). -export default async function (container: MedusaContainer) { - const logger = container.resolve("logger") +You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: - const { result } = await syncBrandsFromCmsWorkflow(container).run() +![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) - logger.info( - `Synced brands from third-party system: ${ - result.created.length - } brands created and ${result.updated.length} brands updated.`) -} +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" -export const config = { - name: "sync-brands-from-system", - schedule: "0 0 * * *", // change to * * * * * for debugging -} +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) ``` -A scheduled job file must export: +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. -- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. -- An object of scheduled jobs configuration. It has two properties: - - `name`: A unique name for the scheduled job. - - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. +You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: -The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" -Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. +type PostAdminCreateBrandType = z.infer + +// ... +``` *** -## Test it Out +## 3. Add Validation Middleware -To test out the scheduled job, start the Medusa application: +A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. -```bash npm2yarn -npm run dev -``` +Learn more about middlewares in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). -If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. +Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. -*** +Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: -## Summary +![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) -By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" -With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/brands", + method: "POST", + middlewares: [ + validateAndTransformBody(PostAdminCreateBrand), + ], + }, + ], +}) +``` +You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. -# Guide: Implement Brand Module +In the middleware object, you define three properties: -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. +- `method`: The HTTP method to restrict the middleware to, which is `POST`. +- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. -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. +The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. -In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. +*** -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +## Test API Route -## 1. Create Module Directory +To test out the API route, start the Medusa application with the following command: -Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. +```bash npm2yarn +npm run dev +``` -![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) +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: -## 2. Create Data Model +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` -A data model represents a table in the database. You create data models using Medusa's Data Model Language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. +Make sure to replace the email and password with your admin user's credentials. -Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/index.html.md). +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). -You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content: +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: -![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" +This returns the created brand in the response: -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), -}) +```json title="Example Response" +{ + "brand": { + "id": "01J7AX9ES4X113HKY6C681KDZJ", + "name": "Acme", + "created_at": "2024-09-09T08:09:34.244Z", + "updated_at": "2024-09-09T08:09:34.244Z" + } +} ``` -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. +*** -You define the data model using the `define` method of the DML. It accepts two parameters: +## Summary -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. +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/property-types/index.html.md). +1. Creating a module that defines and manages a `brand` table in the database. +2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. +3. Creating an API route that allows admin users to create a brand. *** -## 3. Create Module Service +## Next Steps: Associate Brand with Product -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. +Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). -In this step, you'll create the Brand Module's service that provides methods to manage the `Brand` data model. In the next chapters, you'll use this service when exposing custom features that involve managing brands. +In the next chapters, you'll learn how to build associations between data models defined in different modules. -Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). -You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: +# Write Tests for Modules -![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) +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. -```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" +### Prerequisites -class BrandModuleService extends MedusaService({ - Brand, -}) { +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -} +## moduleIntegrationTestRunner Utility -export default BrandModuleService -``` +`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. -The `BrandModuleService` extends a class returned by `MedusaService` from the Modules SDK. This function generates a class with data-management methods for your module's data models. +For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: -The `MedusaService` function receives an object of the module's data models as a parameter, and generates methods to manage those data models. So, the `BrandModuleService` now has methods like `createBrands` and `retrieveBrand` to manage the `Brand` data model. +```ts title="src/modules/hello/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { HELLO_MODULE } from ".." +import HelloModuleService from "../service" +import MyCustom from "../models/my-custom" -You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). +moduleIntegrationTestRunner({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + // TODO write tests + }, +}) -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). +jest.setTimeout(60 * 1000) +``` -*** +The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: -## 4. Export Module Definition +- `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 model. +- `testSuite`: A function that defines the tests to run. -A module must export a definition that tells Medusa the name of the module and its main service. This definition is exported in an `index.ts` file at the module's root directory. +The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. -So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: +The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. -![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) +The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" +*** -export const BRAND_MODULE = "brand" +## Run Tests -export default Module(BRAND_MODULE, { - service: BrandModuleService, -}) -``` +Run the following command to run your module integration tests: -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: +```bash npm2yarn +npm run test:integration:modules +``` -1. The module's name (`brand`). You'll use this name when you use this module in other customizations. -2. An object with a required property `service` indicating the module's main service. +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). -You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. *** -## 5. Add Module to Medusa's Configurations +## Pass Module Options -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. +If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. -The object passed to `defineConfig` in `medusa-config.ts` accepts a `modules` property, whose value is an array of modules to add to the application. So, add the following in `medusa-config.ts`: +For example: -```ts title="medusa-config.ts" -module.exports = defineConfig({ +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" + +moduleIntegrationTestRunner({ + moduleOptions: { + apiKey: "123", + }, // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], }) ``` -The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). - *** -## 6. Generate and Run Migrations +## Write Tests for Modules without Data Models -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations ensure that your module is re-usable and removes friction when working in a team, making it easy to reflect changes across team members' databases. +If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). +For example: -[Medusa's CLI tool](https://docs.medusajs.com/resources/medusa-cli/index.html.md) allows you to generate migration files for your module, then run those migrations to reflect the changes in the database. So, run the following commands in your Medusa application's directory: +```ts +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import HelloModuleService from "../service" +import { model } from "@medusajs/framework/utils" -```bash -npx medusa db:generate brand -npx medusa db:migrate -``` +const DummyModel = model.define("dummy_model", { + id: model.id().primaryKey(), +}) -The `db:generate` command accepts as an argument the name of the module to generate the migrations for, and the `db:migrate` command runs all migrations that haven't been run yet in the Medusa application. +moduleIntegrationTestRunner({ + moduleModels: [DummyModel], + // ... +}) + +jest.setTimeout(60 * 1000) +``` *** -## Next Step: Create Brand Workflow +### Other Options and Inputs -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. +Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. -In the next chapter, you'll implement the functionality to create a brand in a workflow. You'll then use that workflow in a later chapter to expose an endpoint that allows admin users to create a brand. +*** +## Database Used in Tests -# Guide: Integrate CMS Brand System +The `moduleIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. -In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. +To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -## 1. Create Module Directory +# Guide: Create Brand Workflow -You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. +This chapter builds on the work from the [previous chapter](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) where you created a Brand Module. -![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) +After adding custom modules to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. -*** +The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. -## 2. Create Module Service +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. +### Prerequisites -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) +*** -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" +## 1. Create createBrandStep -export type ModuleOptions = { - apiKey: string -} +A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using `createStep` from the Workflows SDK -type InjectedDependencies = { - logger: Logger - configModule: ConfigModule -} +The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: -class CmsModuleService { - private options_: ModuleOptions - private logger_: Logger +![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) - constructor({ logger }: InjectedDependencies, options: ModuleOptions) { - this.logger_ = logger - this.options_ = options +```ts title="src/workflows/create-brand.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { BRAND_MODULE } from "../modules/brand" +import BrandModuleService from "../modules/brand/service" - // TODO initialize SDK - } +export type CreateBrandStepInput = { + name: string } -export default CmsModuleService +export const createBrandStep = createStep( + "create-brand-step", + async (input: CreateBrandStepInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brand = await brandModuleService.createBrands(input) + + return new StepResponse(brand, brand.id) + } +) ``` -You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: +You create a `createBrandStep` using the `createStep` function. It accepts the step's unique name as a first parameter, and the step's function as a second parameter. -1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. -2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. +The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. -When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. +The [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. -### Integration Methods +So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. -Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. +Learn more about the generated `create` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/create/index.html.md). -Add the following methods in the `CmsModuleService`: +A step must return an instance of `StepResponse`. Its first parameter is the data returned by the step, and the second is the data passed to the compensation function, which you'll learn about next. -```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} -export class CmsModuleService { - // ... +### Add Compensation Function to Step - // a dummy method to simulate sending a request, - // in a realistic scenario, you'd use an SDK, fetch, or axios clients - private async sendRequest(url: string, method: string, data?: any) { - this.logger_.info(`Sending a ${method} request to ${url}.`) - this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) - this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) - } +You define for each step a compensation function that's executed when an error occurs in the workflow. The compensation function defines the logic to roll-back the changes made by the step. This ensures your data remains consistent if an error occurs, which is especially useful when you integrate third-party services. - async createBrand(brand: Record) { - await this.sendRequest("/brands", "POST", brand) - } +Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - async deleteBrand(id: string) { - await this.sendRequest(`/brands/${id}`, "DELETE") - } +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: - async retrieveBrands(): Promise[]> { - await this.sendRequest("/brands", "GET") +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( + // ... + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) - return [] + await brandModuleService.deleteBrands(id) } -} +) ``` -The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. - -You also add three methods that use the `sendRequest` method: - -- `createBrand` that creates a brand in the third-party system. -- `deleteBrand` that deletes the brand in the third-party system. -- `retrieveBrands` to retrieve a brand from the third-party system. - -*** +The compensation function's first parameter is the brand's ID which you passed as a second parameter to the step function's returned `StepResponse`. It also accepts a context object with a `container` property as a second parameter, similar to the step function. -## 3. Export Module Definition +In the compensation function, you resolve the Brand Module's service from the Medusa container, then use its generated `deleteBrands` method to delete the brand created by the step. This method accepts the ID of the brand to delete. -After creating the module's service, you'll export the module definition indicating the module's name and service. +Learn more about the generated `delete` method's usage in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/delete/index.html.md). -Create the file `src/modules/cms/index.ts` with the following content: +So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. -![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) +*** -```ts title="src/modules/cms/index.ts" -import { Module } from "@medusajs/framework/utils" -import CmsModuleService from "./service" +## 2. Create createBrandWorkflow -export const CMS_MODULE = "cms" +You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use `createWorkflow` from the Workflows SDK to create the workflow. -export default Module(CMS_MODULE, { - service: CmsModuleService, -}) -``` +Add the following content in the same `src/workflows/create-brand.ts` file: -You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -*** +// ... -## 4. Add Module to Medusa's Configurations +type CreateBrandWorkflowInput = { + name: string +} -Finally, add the module to the Medusa configurations at `medusa-config.ts`: +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - // ... - { - resolve: "./src/modules/cms", - options: { - apiKey: process.env.CMS_API_KEY, - }, - }, - ], -}) + return new WorkflowResponse(brand) + } +) ``` -The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. +You create the `createBrandWorkflow` using the `createWorkflow` function. This function accepts two parameters: the workflow's unique name, and the workflow's constructor function holding the workflow's implementation. -You can add the `CMS_API_KEY` environment variable to `.env`: +The constructor function accepts the workflow's input as a parameter. In the function, you invoke the `createBrandStep` you created in the previous step to create a brand. -```bash -CMS_API_KEY=123 -``` +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. *** -## Next Steps: Sync Brand From Medusa to CMS +## Next Steps: Expose Create Brand API Route -You can now use the CMS Module's service to perform actions on the third-party CMS. +You now have a `createBrandWorkflow` that you can execute to create a brand. -In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. +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. # Create Brands UI Route in Admin @@ -3763,7 +3496,7 @@ You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` - `fields`: A comma-separated string indicating the fields to retrieve. - `limit`: The maximum number of items to retrieve. - `offset`: The number of items to skip before retrieving the returned items. - - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order/index.html.md) + - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) - An object of Query configurations having the following properties: - `defaults`: An array of default fields and relations to retrieve. - `isList`: Whether the API route returns a list of items. @@ -4143,7 +3876,7 @@ A widget's file must export: 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/index.html.md), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. +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. @@ -4178,3367 +3911,3275 @@ The [Admin Components guides](https://docs.medusajs.com/resources/admin-componen In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. -# Write Integration Tests +# Guide: Define Module Link Between Brand and Product -In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. +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. -### Prerequisites +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. -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +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. -## medusaIntegrationTestRunner Utility +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. -The `medusaIntegrationTestRunner` is from Medusa's Testing Framework and it's used to create integration tests in your Medusa project. It runs a full Medusa application, allowing you test API routes, workflows, or other customizations. +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -For example: +### Prerequisites -```ts title="integration-tests/http/test.spec.ts" highlights={highlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - // TODO write tests... - }, -}) +## 1. Define Link -jest.setTimeout(60 * 1000) -``` +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. -The `medusaIntegrationTestRunner` function accepts an object as a parameter. The object has a required property `testSuite`. +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: -`testSuite`'s value is a function that defines the tests to run. The function accepts as a parameter an object that has the following properties: +![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) -- `api`: a set of utility methods used to send requests to the Medusa application. It has the following methods: - - `get`: Send a `GET` request to an API route. - - `post`: Send a `POST` request to an API route. - - `delete`: Send a `DELETE` request to an API route. -- `getContainer`: a function that retrieves the Medusa Container. Use the `getContainer().resolve` method to resolve resources from the Medusa Container. +```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" -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` -### Jest Timeout +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. -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: +The `defineLink` function accepts two parameters of the same type, which is either: -```ts title="integration-tests/http/test.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +- 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. *** -### Run Tests +## 2. Sync the Link to the Database -Run the following command to run your tests: +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 npm2yarn -npm run test:integration +```bash +npx medusa db:migrate ``` -If you don't have a `test:integration` 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 command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. -This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. *** -## Other Options and Inputs +## Next Steps: Extend Create Product Flow -Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +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. -*** -## Database Used in Tests +# Guide: Extend Create Product Flow -The `medusaIntegrationTestRunner` function creates a database with a random name before running the tests. Then, it drops that database after all the tests end. +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. -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/medusaIntegrationTestRunner/index.html.md). +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: -## Example Integration Tests +- 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`. -The next chapters provide examples of writing integration tests for API routes and workflows. +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 -# Write Tests for Modules +- [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) -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 +## 1. Consume the productCreated Hook -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +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. -## moduleIntegrationTestRunner Utility +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -`moduleIntegrationTestRunner` creates integration tests for a module. The integration tests run on a test Medusa application with only the specified module enabled. +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. -For example, assuming you have a `hello` module, create a test file at `src/modules/hello/__tests__/service.spec.ts`: +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: -```ts title="src/modules/hello/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { HELLO_MODULE } from ".." -import HelloModuleService from "../service" -import MyCustom from "../models/my-custom" +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) -moduleIntegrationTestRunner({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - // TODO write tests - }, -}) +```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" -jest.setTimeout(60 * 1000) +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 + }) +) ``` -The `moduleIntegrationTestRunner` function accepts as a parameter an object with the following properties: +Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productCreated`, accepts a step function as a parameter. The step function accepts the following parameters: -- `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 model. -- `testSuite`: A function that defines the tests to run. +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. -The `testSuite` function accepts as a parameter an object having the `service` property, which is an instance of the module's main service. +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. -The type argument provided to the `moduleIntegrationTestRunner` function is used as the type of the `service` property. +### Link Brand to Product -The tests in the `testSuite` function are written using [Jest](https://jestjs.io/). +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). -## Run Tests +To use Link in the `productCreated` hook, replace the `TODO` with the following: -Run the following command to run your module integration tests: +```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} +const link = container.resolve("link") +const logger = container.resolve("logger") -```bash npm2yarn -npm run test:integration:modules +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) ``` -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). +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. -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +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) -## Pass Module Options +Finally, you return an instance of `StepResponse` returning the created links. -If your module accepts options, you can set them using the `moduleOptions` property of the `moduleIntegrationTestRunner`'s parameter. +### Dismiss Links in Compensation -For example: +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. -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: -moduleIntegrationTestRunner({ - moduleOptions: { - apiKey: "123", - }, +```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. + *** -## Write Tests for Modules without Data Models +## 2. Configure Additional Data Validation -If your module doesn't have a data model, pass a dummy model in the `moduleModels` property. +Now that you've consumed the `productCreated` 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. -For example: +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: -```ts -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import HelloModuleService from "../service" -import { model } from "@medusajs/framework/utils" +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) -const DummyModel = model.define("dummy_model", { - id: model.id().primaryKey(), -}) +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" -moduleIntegrationTestRunner({ - moduleModels: [DummyModel], - // ... -}) +// ... -jest.setTimeout(60 * 1000) +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) ``` -*** - -### Other Options and Inputs +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/). -Refer to [this reference in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md) for other available parameter options and inputs of the `testSuite` function. +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. *** -## 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. +## Test it Out -To manage that database, such as changing its name or perform operations on it in your tests, refer to the [references in the Development Resources documentation](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). +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" +}' +``` -# Admin Development Constraints +Make sure to replace the email and password in the request body with your user's credentials. -This chapter lists some constraints of admin widgets and UI routes. +Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: -## Arrow Functions +```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"] + } + ], + "additional_data": { + "brand_id": "{brand_id}" + } +}' +``` -Widget and UI route components must be created as arrow functions. +Make sure to replace `{token}` with the token you received from the previous request, and `{brand_id}` with the ID of a brand in your application. -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { - // ... -} +The request creates a product and returns it. -// Do -const ProductWidget = () => { - // ... -} -``` +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. *** -## Widget Zone +## Next Steps: Query Linked Brands and Products -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. +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. -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) +# Guide: Sync Brands from Medusa to CMS -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` +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. -# Environment Variables in Admin Customizations +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. -In this chapter, you'll learn how to use environment variables in your admin customizations. +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -To learn how envirnment 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 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. -## How to Set Environment Variables +### Prerequisites -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 -``` +- [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 -## How to Use Environment Variables +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. -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. +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`: -For example: +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -```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 createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) -export default ProductWidget + return new WorkflowResponse(brand) + } +) ``` -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: +The `emitEventStep` accepts an object parameter having two properties: -```ts title="src/admin/vite-env.d.ts" -/// -``` +- `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. -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +You'll learn how to handle this event in a later step. *** -## Check Node Environment in Admin Customizations - -To check the current environment, Vite exposes two variables: +## 2. Create Sync to Third-Party System Workflow -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. +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. -Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). +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). -# Admin Development Tips +You'll create a `syncBrandToSystemWorkflow` that has two steps: -In this chapter, you'll find some tips for your admin development. +- `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. -## Send Requests to API Routes +### syncBrandToCmsStep -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. +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: -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. +![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) -First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: +```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" -```ts -import Medusa from "@medusajs/js-sdk" +type SyncBrandToCmsStepInput = { + brand: InferTypeOf +} -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) -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). + await cmsModuleService.createBrand(brand) -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) -For example: + await cmsModuleService.deleteBrand(id) + } +) +``` -### Query +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. -```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" +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. -const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - - return ( - - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) +### Create Workflow -export default ProductWidget -``` +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: -### Mutation +```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" -```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"), - }) +type SyncBrandToCmsWorkflowInput = { + id: string +} - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", +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, + }, }) - } - - return ( - - - - ) -} -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) -export default ProductWidget + return new WorkflowResponse({}) + } +) ``` -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). +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. -## Routing Functionalities +You'll execute this workflow in the subscriber next. -To navigate or link to other pages, or perform other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package. It's installed in your project through the Medusa Admin. +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). -For example: +*** -```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "@medusajs/ui" -import { Link } from "react-router-dom" +## 3. Handle brand.created Event -// The widget -const ProductWidget = () => { - return ( - - View Orders - - ) -} +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. -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +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: -export default ProductWidget -``` +![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) -This adds a widget in a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix. - -Refer to [react-router-dom’s documentation](https://reactrouter.com/en/main) for other available components and hooks. - -*** - -## Admin Translations +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} +import type { + SubscriberConfig, + SubscriberArgs, +} from "@medusajs/framework" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. +export default async function brandCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await syncBrandToCmsWorkflow(container).run({ + input: data, + }) +} -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). +export const config: SubscriberConfig = { + event: "brand.created", +} +``` +A subscriber file must export: -# Admin UI Routes +- 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. -In this chapter, you’ll learn how to create a UI route in the admin dashboard. +The subscriber function accepts an object parameter that has two properties: -## What is a UI Route? +- `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. -The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. +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. -For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). *** -## How to Create a UI Route? - -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component. +## Test it Out -For example, create the file `src/admin/routes/custom/page.tsx` with the following content: +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. -![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) +First, start the Medusa application: -```tsx title="src/admin/routes/custom/page.tsx" -import { Container, Heading } from "@medusajs/ui" +```bash npm2yarn +npm run dev +``` -const CustomPage = () => { - return ( - -
- This is my custom route -
-
- ) -} +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: -export default CustomPage +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' ``` -You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading. +Make sure to replace the email and password with your admin user's credentials. -In the route, 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. +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). -The UI route component must be created as an arrow function. +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: -### Test the UI Route +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` -To test the UI route, start the Medusa application: +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: -```bash npm2yarn -npm run dev +```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" ``` -Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page. - *** -## Show UI Route in the Sidebar +## Next Chapter: Sync Brand from Third-Party CMS to Medusa -To add a sidebar item for your custom UI route, export a configuration object in the UI route's file: +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. -```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container, Heading } from "@medusajs/ui" -const CustomPage = () => { - return ( - -
- This is my custom route -
-
- ) -} +# Guide: Query Product's Brands -export const config = defineRouteConfig({ - label: "Custom Route", - icon: ChatBubbleLeftRight, -}) +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. -export default CustomPage -``` +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. -The configuration object is created using `defineRouteConfig` from the Medusa Framework. It accepts the following properties: +### Prerequisites -- `label`: the sidebar item’s label. -- `icon`: an optional React component used as an icon in the sidebar. +- [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) -The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md). +*** -### Nested UI Routes +## Approach 1: Retrieve Brands in Existing API Routes -Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: +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. -![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) +Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations). -```tsx title="src/admin/routes/custom/nested/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +For example, send the following request to retrieve the list of products with their brands: -const NestedCustomPage = () => { - return ( - -
- This is my nested custom route -
-
- ) -} +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` -export const config = defineRouteConfig({ - label: "Nested Route", -}) +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). -export default NestedCustomPage -``` +Any product that is linked to a brand will have a `brand` property in its object: -This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked. +```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 + } +} +``` -#### Caveats +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. -Some caveats for nested UI routes in the sidebar: +*** -- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. -- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. -- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. +## Approach 2: Use Query to Retrieve Linked Records -### Route Under Existing Admin Route +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. -You can add a custom UI route under an existing route. For example, you can add a route under the orders route: +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -```tsx title="src/admin/routes/orders/nested/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +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: -const NestedOrdersPage = () => { - return ( - -
- Nested Orders Page -
-
- ) -} +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -export const config = defineRouteConfig({ - label: "Nested Orders", - nested: "/orders", -}) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) -export default NestedOrdersPage + res.json({ brands }) +} ``` -The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item. +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. -## Create Settings Page +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. -To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`. +### Test it Out -For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`: +To test the API route out, send a `GET` request to `/admin/brands`: -![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg) +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` -```tsx title="src/admin/routes/settings/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +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). -const CustomSettingPage = () => { - return ( - -
- Custom Setting Page -
-
- ) -} - -export const config = defineRouteConfig({ - label: "Custom", -}) +This returns the brands in your store with their linked products. For example: -export default CustomSettingPage +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} ``` -This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`. - *** -## Path Parameters +## Summary -A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. +By following the examples of the previous chapters, you: -For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: +- 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. -![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg) +*** -```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["10", "{id}", "Show the path parameter."]]} -import { useParams } from "react-router-dom" -import { Container, Heading } from "@medusajs/ui" +## Next Steps: Customize Medusa Admin -const CustomPage = () => { - const { id } = useParams() +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. - return ( - -
- Passed ID: {id} -
-
- ) -} +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. -export default CustomPage -``` -You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params). +# Guide: Schedule Syncing Brands from CMS -If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. +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. -*** +However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. -## Admin Components List +You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. -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. +Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. -# Admin Widgets +### Prerequisites -In this chapter, you’ll learn more about widgets and how to use them. +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) -## 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. +## 1. Implement Syncing Workflow -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. -*** +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. -## How to Create a Widget? +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). -### Prerequisites +This workflow will have three steps: -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) +1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. +2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. +3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. -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. +### retrieveBrandsFromCmsStep -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: +To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` 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) +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import CmsModuleService from "../modules/cms/service" +import { CMS_MODULE } from "../modules/cms" -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} +const retrieveBrandsFromCmsStep = createStep( + "retrieve-brands-from-cms", + async (_, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve( + CMS_MODULE + ) -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) + const brands = await cmsModuleService.retrieveBrands() -export default ProductWidget + return new StepResponse(brands) + } +) ``` -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. +You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. -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. +### createBrandsStep -In the example above, the widget is injected at the top of a product’s details. +The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: -The widget component must be created as an arrow function. +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +// other imports... +import BrandModuleService from "../modules/brand/service" +import { BRAND_MODULE } from "../modules/brand" -### Test the Widget +// ... -To test out the widget, start the Medusa application: +type CreateBrand = { + name: string +} -```bash npm2yarn -npm run dev -``` +type CreateBrandsInput = { + brands: CreateBrand[] +} -Then, open a product’s details page. You’ll find your custom widget at the top of the page. +export const createBrandsStep = createStep( + "create-brands-step", + async (input: CreateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -*** + const brands = await brandModuleService.createBrands(input.brands) -## Props Passed in Detail Pages + return new StepResponse(brands, brands) + }, + async (brands, { container }) => { + if (!brands) { + return + } -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + } +) +``` -```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 `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps) => { - return ( - -
- - Product Widget {data.title} - -
-
- ) -} +The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). -export default ProductWidget -``` +### Update Brands Step -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`. +The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: -*** +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} +// ... -## Injection Zone +type UpdateBrand = { + id: string + name: string +} -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. +type UpdateBrandsInput = { + brands: UpdateBrand[] +} -*** +export const updateBrandsStep = createStep( + "update-brands-step", + async ({ brands }: UpdateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -## Admin Components List + const prevUpdatedBrands = await brandModuleService.listBrands({ + id: brands.map((brand) => brand.id), + }) -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. + const updatedBrands = await brandModuleService.updateBrands(brands) + return new StepResponse(updatedBrands, prevUpdatedBrands) + }, + async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } -# Guide: Sync Brands from Medusa to CMS + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) -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. + await brandModuleService.updateBrands(prevUpdatedBrands) + } +) +``` -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. +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. -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. +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +### Create Workflow -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. +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: -### Prerequisites +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... +import { + // ... + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -- [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 +export const syncBrandsFromCmsWorkflow = createWorkflow( + "sync-brands-from-system", + () => { + const brands = retrieveBrandsFromCmsStep() -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. + // TODO create and update brands + } +) +``` -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`: +In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. -// ... +Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... +So, replace the `TODO` with the following: - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, +```ts title="src/workflows/sync-brands-from-cms.ts" +const { toCreate, toUpdate } = transform( + { + brands, + }, + (data) => { + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] + + data.brands.forEach((brand) => { + if (brand.external_id) { + toUpdate.push({ + id: brand.external_id as string, + name: brand.name as string, + }) + } else { + toCreate.push({ + name: brand.name as string, + }) + } }) - return new WorkflowResponse(brand) + return { toCreate, toUpdate } } ) + +// TODO create and update the brands ``` -The `emitEventStep` accepts an object parameter having two properties: +`transform` accepts two parameters: -- `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. +1. The data to be passed to the function in the second parameter. +2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. -You'll learn how to handle this event in a later step. +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. -*** +You now have the list of brands to create and update. So, replace the new `TODO` with the following: -## 2. Create Sync to Third-Party System Workflow +```ts title="src/workflows/sync-brands-from-cms.ts" +const created = createBrandsStep({ brands: toCreate }) +const updated = updateBrandsStep({ brands: toUpdate }) -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. +return new WorkflowResponse({ + created, + updated, +}) +``` -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. +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. -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. +## 2. Schedule Syncing Task -### syncBrandToCmsStep +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. -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: +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-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) +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.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" +```ts title="src/jobs/sync-brands-from-cms.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" -type SyncBrandToCmsStepInput = { - brand: InferTypeOf +export default async function (container: MedusaContainer) { + const logger = container.resolve("logger") + + const { result } = await syncBrandsFromCmsWorkflow(container).run() + + logger.info( + `Synced brands from third-party system: ${ + result.created.length + } brands created and ${result.updated.length} brands updated.`) } -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) +export const config = { + name: "sync-brands-from-system", + schedule: "0 0 * * *", // change to * * * * * for debugging +} +``` - await cmsModuleService.createBrand(brand) +A scheduled job file must export: - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) +The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. - await cmsModuleService.deleteBrand(id) - } -) -``` +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. -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. +## Test it Out -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). +To test out the scheduled job, start the Medusa application: -### Create Workflow +```bash npm2yarn +npm run dev +``` -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: +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. -```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" +*** -// ... +## Summary -type SyncBrandToCmsWorkflowInput = { - id: string -} +By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. -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, - }, - }) +With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - return new WorkflowResponse({}) - } -) -``` +# Guide: Integrate CMS Brand System -You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: +In the previous chapters, you've created a [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. -- `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. +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -You'll execute this workflow in the subscriber next. +## 1. Create Module Directory -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). +You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. + +![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) *** -## 3. Handle brand.created Event +## 2. Create Module Service -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. +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. -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: +Create the CMS Module's service at `src/modules/cms/service.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) +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.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" +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" -export default async function brandCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncBrandToCmsWorkflow(container).run({ - input: data, - }) +export type ModuleOptions = { + apiKey: string } -export const config: SubscriberConfig = { - event: "brand.created", +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule } -``` -A subscriber file must export: +class CmsModuleService { + private options_: ModuleOptions + private logger_: Logger -- 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. + constructor({ logger }: InjectedDependencies, options: ModuleOptions) { + this.logger_ = logger + this.options_ = options -The subscriber function accepts an object parameter that has two properties: + // TODO initialize SDK + } +} -- `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). - -*** +export default CmsModuleService +``` -## Test it Out +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: -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. +1. The module's container. Since a module is [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), it has a [local container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md) and resources within the module. +2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. -First, start the Medusa application: +When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. -```bash npm2yarn -npm run dev -``` +### Integration Methods -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: +Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` +Add the following methods in the `CmsModuleService`: -Make sure to replace the email and password with your admin user's credentials. +```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} +export class CmsModuleService { + // ... -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + // a dummy method to simulate sending a request, + // in a realistic scenario, you'd use an SDK, fetch, or axios clients + private async sendRequest(url: string, method: string, data?: any) { + this.logger_.info(`Sending a ${method} request to ${url}.`) + this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) + } -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + async createBrand(brand: Record) { + await this.sendRequest("/brands", "POST", brand) + } -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` + async deleteBrand(id: string) { + await this.sendRequest(`/brands/${id}`, "DELETE") + } -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: + async retrieveBrands(): Promise[]> { + await this.sendRequest("/brands", "GET") -```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 + return [] + } } -info: API Key: "123" ``` -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa +The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. -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. +You also add three methods that use the `sendRequest` method: +- `createBrand` that creates a brand in the third-party system. +- `deleteBrand` that deletes the brand in the third-party system. +- `retrieveBrands` to retrieve a brand from the third-party system. -# 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. +## 3. Export Module Definition -## Why Pass Additional Data? +After creating the module's service, you'll export the module definition indicating the module's name and service. -Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks. +Create the file `src/modules/cms/index.ts` with the following content: -This is useful when you have a link from your custom module to a commerce module, and you want to perform an additional action when a request is sent to an existing API route. +![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) -For example, the [Create Product API Route](https://docs.medusajs.com/api/admin#products_postproducts/index.html.md) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product. +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" -### API Routes Accepting Additional Data +export const CMS_MODULE = "cms" -### API Routes List +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) +``` -- Campaigns - - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns/index.html.md) - - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid/index.html.md) -- Cart - - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts/index.html.md) - - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid/index.html.md) -- Collections - - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections/index.html.md) - - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid/index.html.md) -- Customers - - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers/index.html.md) - - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid/index.html.md) - - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses/index.html.md) - - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id/index.html.md) -- Draft Orders - - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders/index.html.md) -- Orders - - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete/index.html.md) - - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel/index.html.md) - - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments/index.html.md) - - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments/index.html.md) -- Products - - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts/index.html.md) - - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid/index.html.md) - - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants/index.html.md) - - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id/index.html.md) - - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions/index.html.md) - - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id/index.html.md) -- Product Tags - - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags/index.html.md) - - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid/index.html.md) -- Product Types - - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes/index.html.md) - - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid/index.html.md) -- Promotions - - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions/index.html.md) - - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid/index.html.md) +You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. *** -## How to Pass Additional Data - -### 1. Specify Validation of Additional Data - -Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. - -To do that, use the middleware route object defined in `src/api/middlewares.ts`. - -For example, create the file `src/api/middlewares.ts` with the following content: +## 4. Add Module to Medusa's Configurations -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +Finally, add the module to the Medusa configurations at `medusa-config.ts`: -export default defineMiddlewares({ - routes: [ +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... { - method: "POST", - matcher: "/admin/products", - additionalDataValidator: { - brand: z.string().optional(), + resolve: "./src/modules/cms", + options: { + apiKey: process.env.CMS_API_KEY, }, }, - ], + ], }) ``` -The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property. +The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. -In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. +You can add the `CMS_API_KEY` environment variable to `.env`: -Refer to [Zod's documentation](https://zod.dev) for all available validation rules. +```bash +CMS_API_KEY=123 +``` -### 2. Pass the Additional Data in a Request +*** -You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. +## Next Steps: Sync Brand From Medusa to CMS -For example: +You can now use the CMS Module's service to perform actions on the third-party CMS. -```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"] - } - ], - "additional_data": { - "brand": "Acme" - } -}' -``` +In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. -Make sure to replace the `{token}` in the authorization header with an admin user's authentication token. -In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. +# Environment Variables in Admin Customizations -The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. +In this chapter, you'll learn how to use environment variables in your admin customizations. -*** +To learn how envirnment 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). -## Use Additional Data in a Hook +## How to Set Environment Variables -Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +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_`. -Step functions consuming the workflow hook can access the `additional_data` in the first parameter. +For example: -For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. +```plain +VITE_MY_API_KEY=sk_123 +``` -To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: +*** -```ts title="src/workflows/hooks/product-created.ts" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { Modules } from "@medusajs/framework/utils" +## How to Use Environment Variables -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand) { - return - } +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. - const productModuleService = container.resolve( - Modules.PRODUCT - ) +For example: - await productModuleService.upsertProducts( - products.map((product) => ({ - ...product, - metadata: { - ...product.metadata, - brand: additional_data.brand, - }, - })) - ) +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" - return new StepResponse(products, { - products, - additional_data, - }) - } -) +const ProductWidget = () => { + return ( + +
+ API Key: {import.meta.env.VITE_MY_API_KEY} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -This consumes the `productsCreated` hook, which runs after the products are created. +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. -If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property. +### Type Error on import.meta.env -### Compensation Function +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: -Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. +```ts title="src/admin/vite-env.d.ts" +/// +``` -For example, pass the following second parameter to the `productsCreated` hook: +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. -```ts title="src/workflows/hooks/product-created.ts" -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // ... - }, - async ({ products, additional_data }, { container }) => { - if (!additional_data.brand) { - return - } - - const productModuleService = container.resolve( - Modules.PRODUCT - ) +*** - await productModuleService.upsertProducts( - products - ) - } -) -``` +## Check Node Environment in Admin Customizations -This updates the products to their original state before adding the brand to their `metadata` property. +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. -# Handling CORS in API Routes +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). -In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. -## CORS Overview +# Admin Development Tips -Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. +In this chapter, you'll find some tips for your admin development. -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. +## Send Requests to API Routes -### CORS Configurations +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. -The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. +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. -These configurations accept a URL pattern to identify allowed origins. +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: -For example: +```ts +import Medusa from "@medusajs/js-sdk" -```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/resources/references/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). -*** +Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. -## CORS in Store and Admin Routes +For example: -To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. +### Query -For example: +```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" -```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +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 GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", +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 CORS = false +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -This disables the CORS middleware on API Routes at the path `/store/custom`. +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). *** -## 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. +## Routing Functionalities -You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. +To navigate or link to other pages, or perform other routing functionalities, use the [react-router-dom](https://reactrouter.com/en/main) package. It's installed in your project through the Medusa Admin. 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" +```tsx title="src/admin/widgets/product-widget.tsx" highlights={highlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "@medusajs/ui" +import { Link } from "react-router-dom" -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - const configModule: ConfigModule = - req.scope.resolve("configModule") +// The widget +const ProductWidget = () => { + return ( + + View Orders + + ) +} - return cors({ - origin: parseCorsOrigins( - configModule.projectConfig.http.storeCors - ), - credentials: true, - })(req, res, next) - }, - ], - }, - ], +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", }) + +export default ProductWidget ``` -This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. +This adds a widget in a product's details page with a link to the Orders page. The link's path must be without the `/app` prefix. +Refer to [react-router-dom’s documentation](https://reactrouter.com/en/main) for other available components and hooks. -# Throwing and Handling Errors +*** -In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. +## Admin Translations -## Throw MedusaError +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/resources/contribution-guidelines/admin-translations/index.html.md). -The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. -For example: +# Admin Development Constraints -```ts -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" +This chapter lists some constraints of admin widgets and UI routes. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - if (!req.query.q) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The `q` query parameter is required." - ) - } +## Arrow Functions + +Widget and UI route components must be created as arrow functions. + +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} +// Do +const ProductWidget = () => { // ... } ``` -The `MedusaError` class accepts in its constructor two parameters: +*** -1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section. -2. The second is the message to show in the error response. +## Widget Zone -### Error Object in Response +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. -The error object returned in the response has two properties: +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) -- `type`: The error's type. -- `message`: The error message, if available. -- `code`: A common snake-case code. Its values can be: - - `invalid_request_error` for the `DUPLICATE_ERROR` type. - - `api_error`: for the `DB_ERROR` type. - - `invalid_state_error` for `CONFLICT` error type. - - `unknown_error` for any unidentified error type. - - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor. +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) -### MedusaError Types +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` -|Type|Description|Status Code| -|---|---|---|---|---| -|\`DB\_ERROR\`|Indicates a database error.|\`500\`| -|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`| -|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`| -|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`| -|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`| -|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`| -|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`| -|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`| -|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`| -|Other error types|Any other error type results in an |\`500\`| -*** +# Admin UI Routes -## Override Error Handler +In this chapter, you’ll learn how to create a UI route in the admin dashboard. -The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes. +## What is a UI Route? -This error handler will also be used for errors thrown in Medusa's API routes and resources. +The Medusa Admin dashboard is customizable, allowing you to add new pages, called UI routes. You create a UI route as a React component showing custom content that allow admin users to perform custom actions. -For example, create `src/api/middlewares.ts` with the following: +For example, you can add a new page to show and manage product reviews, which aren't available natively in Medusa. -```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports" -import { - defineMiddlewares, - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" +*** -export default defineMiddlewares({ - errorHandler: ( - error: MedusaError | any, - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - res.status(400).json({ - error: "Something happened.", - }) - }, -}) -``` +## How to Create a UI Route? -The `errorHandler` property's value is a function that accepts four parameters: +### Prerequisites -1. The error thrown. Its type can be `MedusaError` or any other thrown error type. -2. A request object of type `MedusaRequest`. -3. A response object of type `MedusaResponse`. -4. A function of type MedusaNextFunction that executes the next middleware in the stack. +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. +You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. The file’s default export must be the UI route’s React component. +For example, create the file `src/admin/routes/custom/page.tsx` with the following content: -# Protected Routes +![Example of UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) -In this chapter, you’ll learn how to create protected routes. +```tsx title="src/admin/routes/custom/page.tsx" +import { Container, Heading } from "@medusajs/ui" -## What is a Protected Route? +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} -A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. +export default CustomPage +``` -*** +You add a new route at `http://localhost:9000/app/custom`. The `CustomPage` component holds the page's content, which currently only shows a heading. -## Default Protected Routes +In the route, 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. -Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. +The UI route component must be created as an arrow function. -Requests to `/admin` must be user-authenticated to access the route. +### Test the UI Route + +To test the UI route, start the Medusa application: + +```bash npm2yarn +npm run dev +``` -Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication/index.html.md) and [Store](https://docs.medusajs.com/api/store#authentication/index.html.md) authentication methods. +Then, after logging into the admin dashboard, open the page `http://localhost:9000/app/custom` to see your custom page. *** -## Protect Custom API Routes +## Show UI Route in the Sidebar -To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. +To add a sidebar item for your custom UI route, export a configuration object in the UI route's file: -For example: +```tsx title="src/admin/routes/custom/page.tsx" highlights={highlights} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container, Heading } from "@medusajs/ui" -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +const CustomPage = () => { + return ( + +
+ This is my custom route +
+
+ ) +} -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [authenticate("user", ["session", "bearer", "api-key"])], - }, - { - matcher: "/custom/customer*", - middlewares: [authenticate("customer", ["session", "bearer"])], - }, - ], +export const config = defineRouteConfig({ + label: "Custom Route", + icon: ChatBubbleLeftRight, }) + +export default CustomPage ``` -The `authenticate` middleware function accepts three parameters: +The configuration object is created using `defineRouteConfig` from the Medusa Framework. It accepts the following properties: -1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users. -2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method. -3. An optional object of configurations accepting the following properties: - - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. - - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. +- `label`: the sidebar item’s label. +- `icon`: an optional React component used as an icon in the sidebar. -*** +The above example adds a new sidebar item with the label `Custom Route` and an icon from the [Medusa UI Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md). -## Authentication Opt-Out +### Nested UI Routes -To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`. +Consider that along the UI route above at `src/admin/routes/custom/page.tsx` you create a nested UI route at `src/admin/routes/custom/nested/page.tsx` that also exports route configurations: -For example: +![Example of nested UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867243/Medusa%20Book/ui-route-dir-overview_tgju25.jpg) -```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```tsx title="src/admin/routes/custom/nested/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello", - }) +const NestedCustomPage = () => { + return ( + +
+ This is my nested custom route +
+
+ ) } -export const AUTHENTICATE = false -``` +export const config = defineRouteConfig({ + label: "Nested Route", +}) -Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. +export default NestedCustomPage +``` -*** +This UI route is shown in the sidebar as an item nested in the parent "Custom Route" item. Nested items are only shown when the parent sidebar items (in this case, "Custom Route") are clicked. -## Authenticated Request Type +#### Caveats -To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`. +Some caveats for nested UI routes in the sidebar: -The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`. +- Nested dynamic UI routes, such as one created at `src/admin/routes/custom/[id]/page.tsx` aren't added to the sidebar as it's not possible to link to a dynamic route. If the dynamic route exports route configurations, a warning is logged in the browser's console. +- Nested routes in setting pages aren't shown in the sidebar to follow the admin's design conventions. +- The `icon` configuration is ignored for the sidebar item of nested UI route to follow the admin's design conventions. -If you opt-out of authentication in a route as mentioned in the [previous section](#authentication-opt-out), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead. +### Route Under Existing Admin Route -### Retrieve Logged-In Customer's Details +You can add a custom UI route under an existing route. For example, you can add a route under the orders route: -You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. +```tsx title="src/admin/routes/orders/nested/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" -For example: +const NestedOrdersPage = () => { + return ( + +
+ Nested Orders Page +
+
+ ) +} -```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" -import { ICustomerModuleService } from "@medusajs/framework/types" +export const config = defineRouteConfig({ + label: "Nested Orders", + nested: "/orders", +}) -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - if (req.auth_context?.actor_id) { - // retrieve customer - const customerModuleService: ICustomerModuleService = req.scope.resolve( - Modules.CUSTOMER - ) +export default NestedOrdersPage +``` - const customer = await customerModuleService.retrieveCustomer( - req.auth_context.actor_id - ) - } +The `nested` property passed to `defineRouteConfig` specifies which route this custom route is nested under. This route will now show in the sidebar under the existing "Orders" sidebar item. - // ... -} -``` +*** -In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. +## Create Settings Page -### Retrieve Logged-In Admin User's Details +To create a page under the settings section of the admin dashboard, create a UI route under the path `src/admin/routes/settings`. -You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. +For example, create a UI route at `src/admin/routes/settings/custom/page.tsx`: -For example: +![Example of settings UI route file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867435/Medusa%20Book/setting-ui-route-dir-overview_kytbh8.jpg) -```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" -import { IUserModuleService } from "@medusajs/framework/types" - -export const GET = async ( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) => { - const userModuleService: IUserModuleService = req.scope.resolve( - Modules.USER - ) +```tsx title="src/admin/routes/settings/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" - const user = await userModuleService.retrieveUser( - req.auth_context.actor_id +const CustomSettingPage = () => { + return ( + +
+ Custom Setting Page +
+
) - - // ... } -``` -In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. +export const config = defineRouteConfig({ + label: "Custom", +}) +export default CustomSettingPage +``` -# API Route Parameters +This adds a page under the path `/app/settings/custom`. An item is also added to the settings sidebar with the label `Custom`. -In this chapter, you’ll learn about path, query, and request body parameters. +*** ## Path Parameters -To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. +A UI route can accept path parameters if the name of any of the directories in its path is of the format `[param]`. -For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: +For example, create the file `src/admin/routes/custom/[id]/page.tsx` with the following content: -```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +![Example of UI route file with path parameters in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867748/Medusa%20Book/path-param-ui-route-dir-overview_kcfbev.jpg) -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${req.params.id}!`, - }) -} -``` +```tsx title="src/admin/routes/custom/[id]/page.tsx" highlights={[["5", "", "Retrieve the path parameter."], ["10", "{id}", "Show the path parameter."]]} +import { useParams } from "react-router-dom" +import { Container, Heading } from "@medusajs/ui" -The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. +const CustomPage = () => { + const { id } = useParams() -### Multiple Path Parameters + return ( + +
+ Passed ID: {id} +
+
+ ) +} -To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. +export default CustomPage +``` -For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: +You access the passed parameter using `react-router-dom`'s [useParams hook](https://reactrouter.com/en/main/hooks/use-params). -```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +If you run the Medusa application and go to `localhost:9000/app/custom/123`, you'll see `123` printed in the page. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[GET] Hello ${ - req.params.id - } - ${req.params.name}!`, - }) -} -``` +*** -You access the `id` and `name` path parameters using the `req.params` property. +## 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. -## Query Parameters -You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. +# Seed Data with Custom CLI Script -For example: +In this chapter, you'll learn how to seed data using a custom CLI script. -```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +## How to Seed Data -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `Hello ${req.query.name}`, - }) -} -``` +To seed dummy data for development or demo purposes, use a custom CLI script. -The value of `req.query.name` is the value passed in `?name=John`, for example. +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. -### Validate Query Parameters +### Example: Seed Dummy Products -You can apply validation rules on received query parameters to ensure they match specified rules and types. +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: -*** +```bash npm2yarn +npm install --save-dev @faker-js/faker +``` -## Request Body Parameters +Then, create the file `src/scripts/demo-products.ts` with the following content: -The Medusa application parses the body of any request having its `Content-Type` header set to `application/json`. The request body parameters are set in the `MedusaRequest`'s `body` property. +```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" -For example: +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 + ) -```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) -type HelloWorldReq = { - name: string -} + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: `[POST] Hello ${req.body.name}!`, - }) + // TODO seed products } ``` -In this example, you use the `name` request body parameter to create the message in the returned response. - -The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. +So far, in the script, you: -To test it out, send the following request to your Medusa application: +- 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. -```bash -curl -X POST 'http://localhost:9000/hello-world' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "name": "John" -}' -``` +Next, replace the `TODO` with the following: -This returns the following JSON object: +```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)], + }, + })), + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } +}) -```json -{ - "message": "[POST] Hello John!" -} +// TODO seed products ``` -### Validate Body Parameters +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. -You can apply validation rules on received body parameters to ensure they match specified rules and types. +Then, replace the new `TODO` with the following: -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) +logger.info(`Seeded ${products.length} products.`) -# Middlewares +// TODO add inventory levels +``` -In this chapter, you’ll learn about middlewares and how to create them. +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. -## What is a Middleware? +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: -A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") -Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) -As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). +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, +})) -## How to Create a Middleware? +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, +}) -Middlewares are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value. +logger.info("Finished seeding inventory levels data.") +``` -For example: +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - MedusaNextFunction, - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +Then, you generate inventory levels for each inventory item, associating it with the first stock location. -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. - next() - }, - ], - }, - ], -}) -``` +### Test Script -The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: +To test out the script, run the following command in your project's directory: -- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). -- `middlewares`: An array of middleware functions. +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` -In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. +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. -*** -## Test the Middleware +# Pass Additional Data to Medusa's API Route -To test the middleware: +In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. -1. Start the application: +## Why Pass Additional Data? -```bash npm2yarn -npm run dev -``` +Some of Medusa's API Routes accept an `additional_data` parameter whose type is an object. The API Route passes the `additional_data` to the workflow, which in turn passes it to its hooks. -2. Send a request to any API route starting with `/custom`. -3. See the following message in the terminal: +This is useful when you have a link from your custom module to a commerce module, and you want to perform an additional action when a request is sent to an existing API route. -```bash -Received a request! -``` +For example, the [Create Product API Route](https://docs.medusajs.com/api/admin#products_postproducts) accepts an `additional_data` parameter. If you have a data model linked to it, you consume the `productsCreated` hook to create a record of the data model using the custom data and link it to the product. -*** +### API Routes Accepting Additional Data -## When to Use Middlewares +### API Routes List -- You want to protect API routes by a custom condition. -- You're modifying the request body. +- Campaigns + - [Create Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaigns) + - [Update Campaign](https://docs.medusajs.com/api/admin#campaigns_postcampaignsid) +- Cart + - [Create Cart](https://docs.medusajs.com/api/store#carts_postcarts) + - [Update Cart](https://docs.medusajs.com/api/store#carts_postcartsid) +- Collections + - [Create Collection](https://docs.medusajs.com/api/admin#collections_postcollections) + - [Update Collection](https://docs.medusajs.com/api/admin#collections_postcollectionsid) +- Customers + - [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers) + - [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid) + - [Create Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses) + - [Update Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddress_id) +- Draft Orders + - [Create Draft Order](https://docs.medusajs.com/api/admin#draft-orders_postdraftorders) +- Orders + - [Complete Orders](https://docs.medusajs.com/api/admin#orders_postordersidcomplete) + - [Cancel Order's Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idcancel) + - [Create Shipment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillmentsfulfillment_idshipments) + - [Create Fulfillment](https://docs.medusajs.com/api/admin#orders_postordersidfulfillments) +- Products + - [Create Product](https://docs.medusajs.com/api/admin#products_postproducts) + - [Update Product](https://docs.medusajs.com/api/admin#products_postproductsid) + - [Create Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariants) + - [Update Product Variant](https://docs.medusajs.com/api/admin#products_postproductsidvariantsvariant_id) + - [Create Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptions) + - [Update Product Option](https://docs.medusajs.com/api/admin#products_postproductsidoptionsoption_id) +- Product Tags + - [Create Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttags) + - [Update Product Tag](https://docs.medusajs.com/api/admin#product-tags_postproducttagsid) +- Product Types + - [Create Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypes) + - [Update Product Type](https://docs.medusajs.com/api/admin#product-types_postproducttypesid) +- Promotions + - [Create Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotions) + - [Update Promotion](https://docs.medusajs.com/api/admin#promotions_postpromotionsid) *** -## Middleware Function Parameters - -The middleware function accepts three parameters: - -1. A request object of type `MedusaRequest`. -2. A response object of type `MedusaResponse`. -3. A function of type `MedusaNextFunction` that executes the next middleware in the stack. - -You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. +## How to Pass Additional Data -*** +### 1. Specify Validation of Additional Data -## Middleware for Routes with Path Parameters +Before passing custom data in the `additional_data` object parameter, you must specify validation rules for the allowed properties in the object. -To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. +To do that, use the middleware route object defined in `src/api/middlewares.ts`. -For example: +For example, create the file `src/api/middlewares.ts` with the following content: -```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" export default defineMiddlewares({ routes: [ { - matcher: "/custom/:id", - middlewares: [ - // ... - ], + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, }, ], }) ``` -This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`. +The middleware route object accepts an optional parameter `additionalDataValidator` whose value is an object of key-value pairs. The keys indicate the name of accepted properties in the `additional_data` parameter, and the value is [Zod](https://zod.dev/) validation rules of the property. -*** +In this example, you indicate that the `additional_data` parameter accepts a `brand` property whose value is an optional string. -## Restrict HTTP Methods +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. -Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object. +### 2. Pass the Additional Data in a Request -For example: +You can now pass a `brand` property in the `additional_data` parameter of a request to the Create Product API Route. -```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +For example: -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom*", - method: ["POST", "PUT"], - middlewares: [ - // ... - ], - }, - ], -}) +```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"] + } + ], + "additional_data": { + "brand": "Acme" + } +}' ``` -`method`'s value is one or more HTTP methods to apply the middleware to. +Make sure to replace the `{token}` in the authorization header with an admin user's authentication token. -This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`. +In this request, you pass in the `additional_data` parameter a `brand` property and set its value to `Acme`. -*** +The `additional_data` is then passed to hooks in the `createProductsWorkflow` used by the API route. -## Request URLs with Trailing Backslashes +*** -A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. +## Use Additional Data in a Hook -For example, consider you have the following middleware: +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). -```ts collapsibleLines="1-7" expandMoreLabel="Show Imports" -import { - MedusaNextFunction, - MedusaRequest, - MedusaResponse, - defineMiddlewares, -} from "@medusajs/framework/http" +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom", - middlewares: [ - ( - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - console.log("Received a request!") +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. - next() - }, - ], - }, - ], -}) -``` +To do that, create the file `src/workflows/hooks/product-created.ts` with the following content: -If you send a request to `http://localhost:9000/custom`, the middleware will run. +```ts title="src/workflows/hooks/product-created.ts" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { Modules } from "@medusajs/framework/utils" -However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand) { + return + } -In general, avoid adding trailing backslashes when sending requests to API routes. + const productModuleService = container.resolve( + Modules.PRODUCT + ) + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand, + }, + })) + ) -# API Route Response + return new StepResponse(products, { + products, + additional_data, + }) + } +) +``` -In this chapter, you'll learn how to send a response in your API route. +This consumes the `productsCreated` hook, which runs after the products are created. -## Send a JSON Response +If `brand` is passed in `additional_data`, you resolve the Product Module's main service and use its `upsertProducts` method to update the products, adding the brand to the `metadata` property. -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. +### Compensation Function -For example: +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. -```ts title="src/api/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +For example, pass the following second parameter to the `productsCreated` hook: -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, World!", - }) -} -``` +```ts title="src/workflows/hooks/product-created.ts" +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // ... + }, + async ({ products, additional_data }, { container }) => { + if (!additional_data.brand) { + return + } -This API route returns the following JSON object: + const productModuleService = container.resolve( + Modules.PRODUCT + ) -```json -{ - "message": "Hello, World!" -} + await productModuleService.upsertProducts( + products + ) + } +) ``` -*** - -## Set Response Status Code +This updates the products to their original state before adding the brand to their `metadata` property. -By default, setting the JSON data using the `json` method returns a response with a `200` status code. -To change the status code, use the `status` method of the `MedusaResponse` object. +# Admin Widgets -For example: +In this chapter, you’ll learn more about widgets and how to use them. -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +## What is an Admin Widget? -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} -``` +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. -The response of this API route has the status code `201`. +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. *** -## Change Response Content Type +## How to Create a Widget? -To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. +### Prerequisites -For example, to create an API route that returns an event stream: +- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +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. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) +![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) - req.on("end", () => { - clearInterval(interval) - res.end() - }) +```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 ``` -The `writeHead` method accepts two parameters: +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. -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. +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. -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. +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. -## Do More with Responses +### Test the Widget -The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. +To test out the widget, start the Medusa application: +```bash npm2yarn +npm run dev +``` -# Seed Data with Custom CLI Script +Then, open a product’s details page. You’ll find your custom widget at the top of the page. -In this chapter, you'll learn how to seed data using a custom CLI script. +*** -## How to Seed Data +## Props Passed in Detail Pages -To seed dummy data for development or demo purposes, use a custom CLI script. +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. -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. +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: -### Example: Seed Dummy Products +```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" -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -```bash npm2yarn -npm install --save-dev @faker-js/faker +export default ProductWidget ``` -Then, create the file `src/scripts/demo-products.ts` with the following content: +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`. -```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 - ) +## Injection Zone - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) +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. - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 +*** - // TODO seed products -} -``` +## Admin Components List -So far, in the script, you: +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. -- 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: +# Handling CORS in API Routes -```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)], - }, - })), - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } -}) +In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. -// TODO seed products -``` +## CORS Overview -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. +Cross-Origin Resource Sharing (CORS) allows only configured origins to access your API Routes. -Then, replace the new `TODO` with the following: +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. -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) +### CORS Configurations -logger.info(`Seeded ${products.length} products.`) +The `storeCors` and `adminCors` properties of Medusa's `http` configuration set the allowed origins for routes starting with `/store` and `/admin` respectively. -// TODO add inventory levels +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", + // ... + }, + }, +}) ``` -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. +This allows the `http://localhost:7001` origin to access the Admin API Routes, and the `http://localhost:8000` origin to access Store API Routes. -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: +Learn more about the CORS configurations in [this resource guide](https://docs.medusajs.com/resources/references/medusa-config#http/index.html.md). -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") +*** -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) +## CORS in Store and Admin Routes -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) +To disable the CORS middleware for a route, export a `CORS` variable in the route file with its value set to `false`. -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) +For example: -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) +```ts title="src/api/store/custom/route.ts" highlights={[["15"]]} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -logger.info("Finished seeding inventory levels data.") +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} + +export const CORS = false ``` -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. +This disables the CORS middleware on API Routes at the path `/store/custom`. -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. +## CORS in Custom Routes -### Test Script +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 test out the script, run the following command in your project's directory: +You can do that in the exported middlewares configurations in `src/api/middlewares.ts`. -```bash -npx medusa exec ./src/scripts/demo-products.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 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. +This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. -# Request Body and Query Parameter Validation +# HTTP Methods -In this chapter, you'll learn how to validate request body and query parameters in your custom API route. +In this chapter, you'll learn about how to add new API routes for each HTTP method. -## Request Validation +## HTTP Method Handler -Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum. +An API route is created for every HTTP method you export a handler function for in a route file. -Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: +Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. -- `validateAndTransformBody` to validate the request's body parameters against a schema. -- `validateAndTransformQuery` to validate the request's query parameters against a schema. +For example, create the file `src/api/hello-world/route.ts` with the following content: -Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules. +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. +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!", + }) +} +``` -## How to Validate Request Body +This adds two API Routes: -### Step 1: Create Validation Schema +- A `GET` route at `http://localhost:9000/hello-world`. +- A `POST` route at `http://localhost:9000/hello-world`. -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -To create a validation schema with Zod, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes. +# Middlewares -For example, create the file `src/api/custom/validators.ts` with the following content: +In this chapter, you’ll learn about middlewares and how to create them. -```ts title="src/api/custom/validators.ts" -import { z } from "zod" +## What is a Middleware? -export const PostStoreCustomSchema = z.object({ - a: z.number(), - b: z.number(), -}) -``` +A middleware is a function executed when a request is sent to an API Route. It's executed before the route handler function. -The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if: +Middlwares are used to guard API routes, parse request content types other than `application/json`, manipulate request data, and more. -1. It's an object. -2. It has a property `a` that is a required number. -3. It has a property `b` that is a required number. +As Medusa's server is based on Express, you can use any [Express middleware](https://expressjs.com/en/resources/middleware.html). -### Step 2: Add Request Body Validation Middleware +*** -To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter. +## How to Create a Middleware? -For example, create the file `src/api/middlewares.ts` with the following content: +Middlewares are defined in the special file `src/api/middlewares.ts`. Use the `defineMiddlewares` function from the Medusa Framework to define the middlewares, and export its value. + +For example: ```ts title="src/api/middlewares.ts" import { defineMiddlewares, - validateAndTransformBody, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, } from "@medusajs/framework/http" -import { PostStoreCustomSchema } from "./custom/validators" export default defineMiddlewares({ routes: [ { - matcher: "/custom", - method: "POST", + matcher: "/custom*", middlewares: [ - validateAndTransformBody(PostStoreCustomSchema), + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, ], }, ], }) ``` -This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema. +The `defineMiddlewares` function accepts a middleware configurations object that has the property `routes`. `routes`'s value is an array of middleware route objects, each having the following properties: -#### How the Validation Works +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. The regular expression must be compatible with [path-to-regexp](https://github.com/pillarjs/path-to-regexp). +- `middlewares`: An array of middleware functions. -If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors. +In the example above, you define a middleware that logs the message `Received a request!` whenever a request is sent to an API route path starting with `/custom`. -If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`. +*** -### Step 3: Use Validated Body in API Route +## Test the Middleware -In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`. +To test the middleware: -For example, create the file `src/api/custom/route.ts` with the following content: +1. Start the application: -```ts title="src/api/custom/route.ts" highlights={routeHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { z } from "zod" -import { PostStoreCustomSchema } from "./validators" +```bash npm2yarn +npm run dev +``` -type PostStoreCustomSchemaType = z.infer< - typeof PostStoreCustomSchema -> +2. Send a request to any API route starting with `/custom`. +3. See the following message in the terminal: -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - sum: req.validatedBody.a + req.validatedBody.b, - }) -} +```bash +Received a request! ``` -In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties. +*** -To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter. +## When to Use Middlewares -### Test it Out +- You want to protect API routes by a custom condition. +- You're modifying the request body. -To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation. +*** -For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: +## Middleware Function Parameters -```json -{ - "type": "invalid_data", - "message": "Invalid request: Field 'a' is required" -} -``` +The middleware function accepts three parameters: + +1. A request object of type `MedusaRequest`. +2. A response object of type `MedusaResponse`. +3. A function of type `MedusaNextFunction` that executes the next middleware in the stack. + +You must call the `next` function in the middleware. Otherwise, other middlewares and the API route handler won’t execute. *** -## How to Validate Request Query Paramters +## Middleware for Routes with Path Parameters -The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body). +To indicate a path parameter in a middleware's `matcher` pattern, use the format `:{param-name}`. -### Step 1: Create Validation Schema +For example: -The first step is to create a schema with Zod with the rules of the accepted query parameters. +```ts title="src/api/middlewares.ts" collapsibleLines="1-7" expandMoreLabel="Show Imports" highlights={pathParamHighlights} +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" -Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section. +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/:id", + middlewares: [ + // ... + ], + }, + ], +}) +``` -Create the file `src/api/custom/validators.ts` with the following content: +This applies a middleware to the routes defined in the file `src/api/custom/[id]/route.ts`. -```ts title="src/api/custom/validators.ts" -import { z } from "zod" +*** -export const PostStoreCustomSchema = z.object({ - a: z.preprocess( - (val) => { - if (val && typeof val === "string") { - return parseInt(val) - } - return val - }, - z - .number() - ), - b: z.preprocess( - (val) => { - if (val && typeof val === "string") { - return parseInt(val) - } - return val +## Restrict HTTP Methods + +Restrict which HTTP methods the middleware is applied to using the `method` property of the middleware route object. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + MedusaNextFunction, + MedusaRequest, + MedusaResponse, + defineMiddlewares, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom*", + method: ["POST", "PUT"], + middlewares: [ + // ... + ], }, - z - .number() - ), + ], }) ``` -Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers. +`method`'s value is one or more HTTP methods to apply the middleware to. -For both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number. +This example applies the middleware only when a `POST` or `PUT` request is sent to an API route path starting with `/custom`. -### Step 2: Add Request Query Validation Middleware +*** -Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route. +## Request URLs with Trailing Backslashes -Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`: +A middleware whose `matcher` pattern doesn't end with a backslash won't be applied for requests to URLs with a trailing backslash. -```ts title="src/api/middlewares.ts" +For example, consider you have the following middleware: + +```ts collapsibleLines="1-7" expandMoreLabel="Show Imports" import { - validateAndTransformQuery, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, defineMiddlewares, } from "@medusajs/framework/http" -import { PostStoreCustomSchema } from "./custom/validators" export default defineMiddlewares({ routes: [ { matcher: "/custom", - method: "POST", middlewares: [ - validateAndTransformQuery( - PostStoreCustomSchema, - {} - ), + ( + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + console.log("Received a request!") + + next() + }, ], }, ], }) ``` -The `validateAndTransformQuery` accepts two parameters: +If you send a request to `http://localhost:9000/custom`, the middleware will run. -- The first one is the Zod schema to validate the query parameters against. -- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +However, if you send a request to `http://localhost:9000/custom/`, the middleware won't run. -#### How the Validation Works +In general, avoid adding trailing backslashes when sending requests to API routes. -If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors. -If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`. +# Throwing and Handling Errors -### Step 3: Use Validated Query in API Route +In this guide, you'll learn how to throw errors in your Medusa application, how it affects an API route's response, and how to change the default error handler of your Medusa application. -Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters. +## Throw MedusaError -For example, create the file `src/api/custom/route.ts` with the following content: +When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. -```ts title="src/api/custom/route.ts" +The Medusa application's API route error handler then wraps your thrown error in a uniform object and returns it in the response. + +For example: + +```ts import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - const a = req.validatedQuery.a as number - const b = req.validatedQuery.b as number - - res.json({ - sum: a + b, - }) -} -``` - -In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum. - -### Test it Out - -To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works. - -For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: + if (!req.query.q) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The `q` query parameter is required." + ) + } -```json -{ - "type": "invalid_data", - "message": "Invalid request: Field 'a' is required" + // ... } ``` -*** - -## Learn More About Validation Schemas - -To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). +The `MedusaError` class accepts in its constructor two parameters: +1. The first is the error's type. `MedusaError` has a static property `Types` that you can use. `Types` is an enum whose possible values are explained in the next section. +2. The second is the message to show in the error response. -# Add Data Model Check Constraints +### Error Object in Response -In this chapter, you'll learn how to add check constraints to your data model. +The error object returned in the response has two properties: -## What is a Check Constraint? +- `type`: The error's type. +- `message`: The error message, if available. +- `code`: A common snake-case code. Its values can be: + - `invalid_request_error` for the `DUPLICATE_ERROR` type. + - `api_error`: for the `DB_ERROR` type. + - `invalid_state_error` for `CONFLICT` error type. + - `unknown_error` for any unidentified error type. + - For other error types, this property won't be available unless you provide a code as a third parameter to the `MedusaError` constructor. -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. +### MedusaError Types -For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. +|Type|Description|Status Code| +|---|---|---|---|---| +|\`DB\_ERROR\`|Indicates a database error.|\`500\`| +|\`DUPLICATE\_ERROR\`|Indicates a duplicate of a record already exists. For example, when trying to create a customer whose email is registered by another customer.|\`422\`| +|\`INVALID\_ARGUMENT\`|Indicates an error that occurred due to incorrect arguments or other unexpected state.|\`500\`| +|\`INVALID\_DATA\`|Indicates a validation error.|\`400\`| +|\`UNAUTHORIZED\`|Indicates that a user is not authorized to perform an action or access a route.|\`401\`| +|\`NOT\_FOUND\`|Indicates that the requested resource, such as a route or a record, isn't found.|\`404\`| +|\`NOT\_ALLOWED\`|Indicates that an operation isn't allowed.|\`400\`| +|\`CONFLICT\`|Indicates that a request conflicts with another previous or ongoing request. The error message in this case is ignored for a default message.|\`409\`| +|\`PAYMENT\_AUTHORIZATION\_ERROR\`|Indicates an error has occurred while authorizing a payment.|\`422\`| +|Other error types|Any other error type results in an |\`500\`| *** -## How to Set a Check Constraint? +## Override Error Handler -To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. +The `defineMiddlewares` function used to apply middlewares on routes accepts an `errorHandler` in its object parameter. Use it to override the default error handler for API routes. -For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: +This error handler will also be used for errors thrown in Medusa's API routes and resources. -```ts highlights={checks1Highlights} -import { model } from "@medusajs/framework/utils" +For example, create `src/api/middlewares.ts` with the following: -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` +```ts title="src/api/middlewares.ts" collapsibleLines="1-8" expandMoreLabel="Show Imports" +import { + defineMiddlewares, + MedusaNextFunction, + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" -The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. +export default defineMiddlewares({ + errorHandler: ( + error: MedusaError | any, + req: MedusaRequest, + res: MedusaResponse, + next: MedusaNextFunction + ) => { + res.status(400).json({ + error: "Something happened.", + }) + }, +}) +``` -The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. +The `errorHandler` property's value is a function that accepts four parameters: -You can also pass an object to the `checks` method: +1. The error thrown. Its type can be `MedusaError` or any other thrown error type. +2. A request object of type `MedusaRequest`. +3. A response object of type `MedusaResponse`. +4. A function of type MedusaNextFunction that executes the next middleware in the stack. -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" +This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) -``` -The object accepts the following properties: +# API Route Parameters -- `name`: The check constraint's name. -- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). +In this chapter, you’ll learn about path, query, and request body parameters. -*** +## Path Parameters -## Apply in Migrations +To create an API route that accepts a path parameter, create a directory within the route file's path whose name is of the format `[param]`. -After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. +For example, to create an API Route at the path `/hello-world/:id`, where `:id` is a path parameter, create the file `src/api/hello-world/[id]/route.ts` with the following content: -To generate a migration for the data model's module then reflect it on the database, run the following command: +```ts title="src/api/hello-world/[id]/route.ts" highlights={singlePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -```bash -npx medusa db:generate custom_module -npx medusa db:migrate +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${req.params.id}!`, + }) +} ``` -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. - +The `MedusaRequest` object has a `params` property. `params` holds the path parameters in key-value pairs. -# Data Model Default Properties +### Multiple Path Parameters -In this chapter, you'll learn about the properties available by default in your data model. +To create an API route that accepts multiple path parameters, create within the file's path multiple directories whose names are of the format `[param]`. -When you create a data model, the following properties are created for you by Medusa: +For example, to create an API route at `/hello-world/:id/name/:name`, create the file `src/api/hello-world/[id]/name/[name]/route.ts` with the following content: -- `created_at`: A `dateTime` property that stores when a record of the data model was created. -- `updated_at`: A `dateTime` property that stores when a record of the data model was updated. -- `deleted_at`: A `dateTime` property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date. +```ts title="src/api/hello-world/[id]/name/[name]/route.ts" highlights={multiplePathHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[GET] Hello ${ + req.params.id + } - ${req.params.name}!`, + }) +} +``` -# Data Model Database Index +You access the `id` and `name` path parameters using the `req.params` property. -In this chapter, you’ll learn how to define a database index on a data model. +*** -## Define Database Index on Property +## Query Parameters -Use the `index` method on a property's definition to define a database index. +You can access all query parameters in the `query` property of the `MedusaRequest` object. `query` is an object of key-value pairs, where the key is a query parameter's name, and the value is its value. For example: -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), -}) +```ts title="src/api/hello-world/route.ts" highlights={queryHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -export default MyCustom +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `Hello ${req.query.name}`, + }) +} ``` -The `index` method optionally accepts the name of the index as a parameter. +The value of `req.query.name` is the value passed in `?name=John`, for example. -In this example, you define an index on the `name` property. +### Validate Query Parameters + +You can apply validation rules on received query parameters to ensure they match specified rules and types. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-query-paramters/index.html.md). *** -## Define Database Index on Data Model +## Request Body Parameters -A data model has an `indexes` method that defines database indices on its properties. +The Medusa application parses the body of any request having its `Content-Type` header set to `application/json`. The request body parameters are set in the `MedusaRequest`'s `body` property. -The index can be on multiple columns (composite index). For example: +For example: -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/hello-world/route.ts" highlights={bodyHighlights} +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - }, -]) +type HelloWorldReq = { + name: string +} -export default MyCustom +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: `[POST] Hello ${req.body.name}!`, + }) +} ``` -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. - -In the above example, you define a composite index on the `name` and `age` properties. +In this example, you use the `name` request body parameter to create the message in the returned response. -### Index Conditions +The `MedusaRequest` type accepts a type argument that indicates the type of the request body. This is useful for auto-completion and to avoid typing errors. -An index can have conditions. For example: +To test it out, send the following request to your Medusa application: -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" +```bash +curl -X POST 'http://localhost:9000/hello-world' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "name": "John" +}' +``` -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) +This returns the following JSON object: -export default MyCustom +```json +{ + "message": "[POST] Hello John!" +} ``` -The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. +### Validate Body Parameters -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. +You can apply validation rules on received body parameters to ensure they match specified rules and types. -A property's condition can be a negation. For example: +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number().nullable(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: { - $ne: null, - }, - }, - }, -]) +# Request Body and Query Parameter Validation -export default MyCustom -``` +In this chapter, you'll learn how to validate request body and query parameters in your custom API route. -A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. +## Request Validation -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. +Consider you're creating a `POST` API route at `/custom`. It accepts two parameters `a` and `b` that are required numbers, and returns their sum. -### Unique Database Index +Medusa provides two middlewares to validate the request body and query paramters of incoming requests to your custom API routes: -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. +- `validateAndTransformBody` to validate the request's body parameters against a schema. +- `validateAndTransformQuery` to validate the request's query parameters against a schema. -For example: +Both middlewares accept a [Zod](https://zod.dev/) schema as a parameter, which gives you flexibility in how you define your validation schema with complex rules. -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, - }, -]) - -export default MyCustom -``` - -This creates a unique composite index on the `name` and `age` properties. +The next steps explain how to add request body and query parameter validation to the API route mentioned earlier. +*** -# Configure Data Model Properties +## How to Validate Request Body -In this chapter, you’ll learn how to configure data model properties. +### Step 1: Create Validation Schema -## Property’s Default Value +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -Use the `default` method on a property's definition to specify the default value of a property. +To create a validation schema with Zod, create a `validators.ts` file in any `src/api` subfolder. This file holds Zod schemas for each of your API routes. -For example: +For example, create the file `src/api/custom/validators.ts` with the following content: -```ts highlights={defaultHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/custom/validators.ts" +import { z } from "zod" -const MyCustom = model.define("my_custom", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... +export const PostStoreCustomSchema = z.object({ + a: z.number(), + b: z.number(), }) - -export default MyCustom ``` -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. - -*** - -## Nullable Property - -Use the `nullable` method to indicate that a property’s value can be `null`. - -For example: - -```ts highlights={nullableHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - price: model.bigNumber().nullable(), - // ... -}) - -export default MyCustom -``` +The `PostStoreCustomSchema` variable is a Zod schema that indicates the request body is valid if: -*** +1. It's an object. +2. It has a property `a` that is a required number. +3. It has a property `b` that is a required number. -## Unique Property +### Step 2: Add Request Body Validation Middleware -The `unique` method indicates that a property’s value must be unique in the database through a unique index. +To use this schema for validating the body parameters of requests to `/custom`, use the `validateAndTransformBody` middleware provided by `@medusajs/framework/http`. It accepts the Zod schema as a parameter. -For example: +For example, create the file `src/api/middlewares.ts` with the following content: -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostStoreCustomSchema } from "./custom/validators" -const User = model.define("user", { - email: model.text().unique(), - // ... +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: "POST", + middlewares: [ + validateAndTransformBody(PostStoreCustomSchema), + ], + }, + ], }) - -export default User ``` -In this example, multiple users can’t have the same email. +This applies the `validateAndTransformBody` middleware on `POST` requests to `/custom`. It uses the `PostStoreCustomSchema` as the validation schema. +#### How the Validation Works -# Infer Type of Data Model +If a request's body parameters don't pass the validation, the `validateAndTransformBody` middleware throws an error indicating the validation errors. -In this chapter, you'll learn how to infer the type of a data model. +If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the `validatedBody` property of `MedusaRequest`. -## How to Infer Type of Data Model? +### Step 3: Use Validated Body in API Route -Consider you have a `MyCustom` 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. +In your API route, consume the validated body using the `validatedBody` property of `MedusaRequest`. -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. +For example, create the file `src/api/custom/route.ts` with the following content: -For example: +```ts title="src/api/custom/route.ts" highlights={routeHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { z } from "zod" +import { PostStoreCustomSchema } from "./validators" -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" // relative path to the model +type PostStoreCustomSchemaType = z.infer< + typeof PostStoreCustomSchema +> -export type MyCustom = InferTypeOf +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + sum: req.validatedBody.a + req.validatedBody.b, + }) +} ``` -`InferTypeOf` accepts as a type argument the type of the data model. +In the API route, you use the `validatedBody` property of `MedusaRequest` to access the values of the `a` and `b` properties. -Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. +To pass the request body's type as a type parameter to `MedusaRequest`, use Zod's `infer` type that accepts the type of a schema as a parameter. -You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: +### Test it Out -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { MyCustom } from "../models/my-custom" +To test out the validation, send a `POST` request to `/custom` passing `a` and `b` body parameters. You can try sending incorrect request body parameters to test out the validation. -type MyCustom = InferTypeOf +For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: -class HelloModuleService extends MedusaService({ MyCustom }) { - async doSomething(): Promise { - // ... - } +```json +{ + "type": "invalid_data", + "message": "Invalid request: Field 'a' is required" } ``` +*** -# Manage Relationships +## How to Validate Request Query Paramters -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. +The steps to validate the request query parameters are the similar to that of [validating the body](#how-to-validate-request-body). -## Manage One-to-One Relationship +### Step 1: Create Validation Schema -### BelongsTo Side of One-to-One +The first step is to create a schema with Zod with the rules of the accepted query parameters. -When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the relation property. +Consider that the API route accepts two query parameters `a` and `b` that are numbers, similar to the previous section. -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: +Create the file `src/api/custom/validators.ts` with the following content: -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user: "123", -}) +```ts title="src/api/custom/validators.ts" +import { z } from "zod" -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user: "123", +export const PostStoreCustomSchema = z.object({ + a: z.preprocess( + (val) => { + if (val && typeof val === "string") { + return parseInt(val) + } + return val + }, + z + .number() + ), + b: z.preprocess( + (val) => { + if (val && typeof val === "string") { + return parseInt(val) + } + return val + }, + z + .number() + ), }) ``` -In the example above, you pass the `user` property when creating or updating an email to specify the user it belongs to. +Since a query parameter's type is originally a string or array of strings, you have to use Zod's `preprocess` method to validate other query types, such as numbers. -### HasOne Side +For both `a` and `b`, you transform the query parameter's value to an integer first if it's a string, then, you check that the resulting value is a number. -When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. +### Step 2: Add Request Query Validation Middleware -For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: +Next, you'll use the schema to validate incoming requests' query parameters to the `/custom` API route. -```ts highlights={hasOneHighlights} -// when creating a user -const user = await helloModuleService.createUsers({ - // other properties... - email: "123", -}) +Add the `validateAndTransformQuery` middleware to the API route in the file `src/api/middlewares.ts`: -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", +```ts title="src/api/middlewares.ts" +import { + validateAndTransformQuery, + defineMiddlewares, +} from "@medusajs/framework/http" +import { PostStoreCustomSchema } from "./custom/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom", + method: "POST", + middlewares: [ + validateAndTransformQuery( + PostStoreCustomSchema, + {} + ), + ], + }, + ], }) ``` -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. +The `validateAndTransformQuery` accepts two parameters: -*** +- The first one is the Zod schema to validate the query parameters against. +- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -## Manage One-to-Many Relationship +#### How the Validation Works -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. +If a request's query parameters don't pass the validation, the `validateAndTransformQuery` middleware throws an error indicating the validation errors. -When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. +If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the `validatedQuery` property of `MedusaRequest`. -For example, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: +### Step 3: Use Validated Query in API Route -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) +Finally, use the validated query in the API route. The `MedusaRequest` parameter has a `validatedQuery` parameter that you can use to access the validated parameters. -// when updating a product -const product = await helloModuleService.updateProducts({ - id: "321", - // other properties... - store_id: "123", -}) -``` - -In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. - -*** - -## Manage Many-to-Many Relationship - -If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. - -### Create Associations - -When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. +For example, create the file `src/api/custom/route.ts` with the following content: -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -```ts highlights={manyHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - orders: ["123", "321"], -}) +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const a = req.validatedQuery.a as number + const b = req.validatedQuery.b as number -// when creating an order -const order = await helloModuleService.createOrders({ - id: "321", - // other properties... - products: ["123", "321"], -}) + res.json({ + sum: a + b, + }) +} ``` -In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. - -### Update Associations - -When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. - -However, this removes any existing associations to records whose IDs aren't included in the array. - -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: - -```ts -const product = await helloModuleService.updateProducts({ - id: "123", - // other properties... - orders: ["321"], -}) -``` +In the API route, you use the `validatedQuery` property of `MedusaRequest` to access the values of the `a` and `b` properties as numbers, then return in the response their sum. -If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. +### Test it Out -So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: +To test out the validation, send a `POST` request to `/custom` with `a` and `b` query parameters. You can try sending incorrect query parameters to see how the validation works. -```ts highlights={updateAssociationHighlights} -const product = await helloModuleService.retrieveProduct( - "123", - { - relations: ["orders"], - } -) +For example, if you omit the `a` parameter, you'll receive a `400` response code with the following response data: -const updatedProduct = await helloModuleService.updateProducts({ - id: product.id, - // other properties... - orders: [ - ...product.orders.map((order) => order.id), - "321", - ], -}) +```json +{ + "type": "invalid_data", + "message": "Invalid request: Field 'a' is required" +} ``` -This keeps existing associations between the product and orders, and adds a new one. - *** -## Manage Many-to-Many Relationship with pivotEntity +## Learn More About Validation Schemas -If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. +To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). -If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. -For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: +# API Route Response -```ts highlights={["4"]} -class HelloModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` +In this chapter, you'll learn how to send a response in your API route. -This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. +## Send a JSON Response -For example: +To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. -```ts -// create order-product association -const orderProduct = await helloModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) +For example: -// update order-product association -const orderProduct = await helloModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -// delete order-product association -await helloModuleService.deleteOrderProducts("123") +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello, World!", + }) +} ``` -Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. +This API route returns the following JSON object: -Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. +```json +{ + "message": "Hello, World!" +} +``` *** -## Retrieve Records of Relation - -The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. - -To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. +## Set Response Status Code -For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: +By default, setting the JSON data using the `json` method returns a response with a `200` status code. -```ts highlights={retrieveHighlights} -const product = await helloModuleService.retrieveProducts( - "123", - { - relations: ["orders"], - } -) -``` +To change the status code, use the `status` method of the `MedusaResponse` object. -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. +For example: +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -# HTTP Methods +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) +} +``` -In this chapter, you'll learn about how to add new API routes for each HTTP method. +The response of this API route has the status code `201`. -## HTTP Method Handler +*** -An API route is created for every HTTP method you export a handler function for in a route file. +## Change Response Content Type -Allowed HTTP methods are: `GET`, `POST`, `DELETE`, `PUT`, `PATCH`, `OPTIONS`, and `HEAD`. +To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. -For example, create the file `src/api/hello-world/route.ts` with the following content: +For example, to create an API route that returns an event stream: -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - res.json({ - message: "[GET] Hello world!", + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", }) -} -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[POST] Hello world!", + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) + + req.on("end", () => { + clearInterval(interval) + res.end() }) } ``` -This adds two API Routes: +The `writeHead` method accepts two parameters: -- A `GET` route at `http://localhost:9000/hello-world`. -- A `POST` route at `http://localhost:9000/hello-world`. +1. The first one is the response's status code. +2. The second is an object of key-value pairs to set the headers of the response. +This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. -# Data Model’s Primary Key +*** -In this chapter, you’ll learn how to configure the primary key of a data model. +## Do More with Responses -## primaryKey Method +The `MedusaResponse` type is based on [Express's Response](https://expressjs.com/en/api.html#res). Refer to their API reference for other uses of responses. -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. -For example: +# Protected Routes -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" +In this chapter, you’ll learn how to create protected routes. -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - // ... -}) +## What is a Protected Route? -export default MyCustom -``` +A protected route is a route that requires requests to be user-authenticated before performing the route's functionality. Otherwise, the request fails, and the user is prevented access. -In the example above, the `id` property is defined as the data model's primary key. +*** +## Default Protected Routes -# Data Model Property Types +Medusa applies an authentication guard on routes starting with `/admin`, including custom API routes. -In this chapter, you’ll learn about the types of properties in a data model’s schema. +Requests to `/admin` must be user-authenticated to access the route. -## id +Refer to the API Reference for [Admin](https://docs.medusajs.com/api/admin#authentication) and [Store](https://docs.medusajs.com/api/store#authentication) authentication methods. -The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers. +*** -For example: +## Protect Custom API Routes -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id(), - // ... -}) - -export default MyCustom -``` - -*** - -## text - -The `text` method defines a string property. +To protect custom API Routes to only allow authenticated customer or admin users, use the `authenticate` middleware from the Medusa Framework. For example: -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" -const MyCustom = model.define("my_custom", { - name: model.text(), - // ... +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [authenticate("user", ["session", "bearer", "api-key"])], + }, + { + matcher: "/custom/customer*", + middlewares: [authenticate("customer", ["session", "bearer"])], + }, + ], }) - -export default MyCustom ``` -*** - -## number - -The `number` method defines a number property. - -For example: - -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - age: model.number(), - // ... -}) +The `authenticate` middleware function accepts three parameters: -export default MyCustom -``` +1. The type of user authenticating. Use `user` for authenticating admin users, and `customer` for authenticating customers. You can also pass `*` to allow all types of users. +2. An array of types of authentication methods allowed. Both `user` and `customer` scopes support `session` and `bearer`. The `admin` scope also supports the `api-key` authentication method. +3. An optional object of configurations accepting the following properties: + - `allowUnauthenticated`: (default: `false`) A boolean indicating whether authentication is required. For example, you may have an API route where you want to access the logged-in customer if available, but guest customers can still access it too. + - `allowUnregistered` (default: `false`): A boolean indicating if unregistered users should be allowed access. This is useful when you want to allow users who aren’t registered to access certain routes. *** -## float - -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). - -The `float` method defines a number property that allows for values with decimal places. +## Authentication Opt-Out -Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). +To disable the authentication guard on custom routes under the `/admin` path prefix, export an `AUTHENTICATE` variable in the route file with its value set to `false`. For example: -```ts highlights={floatHighlights} -import { model } from "@medusajs/framework/utils" +```ts title="src/api/admin/custom/route.ts" highlights={[["15"]]} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" -const MyCustom = model.define("my_custom", { - rating: model.float(), - // ... -}) +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello", + }) +} -export default MyCustom +export const AUTHENTICATE = false ``` -*** - -## bigNumber - -The `bigNumber` method defines a number property that expects large numbers, such as prices. - -Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). - -For example: - -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - price: model.bigNumber(), - // ... -}) - -export default MyCustom -``` +Now, any request sent to the `/admin/custom` API route is allowed, regardless if the admin user is authenticated. *** -## boolean - -The `boolean` method defines a boolean property. - -For example: - -```ts highlights={booleanHighlights} -import { model } from "@medusajs/framework/utils" +## Authenticated Request Type -const MyCustom = model.define("my_custom", { - hasAccount: model.boolean(), - // ... -}) +To access the authentication details in an API route, such as the logged-in user's ID, set the type of the first request parameter to `AuthenticatedMedusaRequest`. It extends `MedusaRequest`. -export default MyCustom -``` +The `auth_context.actor_id` property of `AuthenticatedMedusaRequest` holds the ID of the authenticated user or customer. If there isn't any authenticated user or customer, `auth_context` is `undefined`. -*** +If you opt-out of authentication in a route as mentioned in the [previous section](#authentication-opt-out), you can't access the authenticated user or customer anymore. Use the [authenticate middleware](#protect-custom-api-routes) instead. -### enum +### Retrieve Logged-In Customer's Details -The `enum` method defines a property whose value can only be one of the specified values. +You can access the logged-in customer’s ID in all API routes starting with `/store` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. For example: -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - color: model.enum(["black", "white"]), - // ... -}) - -export default MyCustom -``` - -The `enum` method accepts an array of possible string values. - -*** - -## dateTime - -The `dateTime` method defines a timestamp property. +```ts title="src/api/store/custom/route.ts" highlights={[["19", "req.auth_context.actor_id", "Access the logged-in customer's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { ICustomerModuleService } from "@medusajs/framework/types" -For example: +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + if (req.auth_context?.actor_id) { + // retrieve customer + const customerModuleService: ICustomerModuleService = req.scope.resolve( + Modules.CUSTOMER + ) -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" + const customer = await customerModuleService.retrieveCustomer( + req.auth_context.actor_id + ) + } -const MyCustom = model.define("my_custom", { - date_of_birth: model.dateTime(), // ... -}) - -export default MyCustom +} ``` -*** +In this example, you resolve the Customer Module's main service, then use it to retrieve the logged-in customer, if available. -## json +### Retrieve Logged-In Admin User's Details -The `json` method defines a property whose value is a stringified JSON object. +You can access the logged-in admin user’s ID in all API Routes starting with `/admin` using the `auth_context.actor_id` property of the `AuthenticatedMedusaRequest` object. For example: -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - metadata: model.json(), - // ... -}) - -export default MyCustom -``` - -*** - -## array - -The `array` method defines an array of strings property. +```ts title="src/api/admin/custom/route.ts" highlights={[["17", "req.auth_context.actor_id", "Access the logged-in admin user's ID."]]} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" +import { IUserModuleService } from "@medusajs/framework/types" -For example: +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const userModuleService: IUserModuleService = req.scope.resolve( + Modules.USER + ) -```ts highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" + const user = await userModuleService.retrieveUser( + req.auth_context.actor_id + ) -const MyCustom = model.define("my_custom", { - names: model.array(), // ... -}) - -export default MyCustom +} ``` -*** - -## Properties Reference - -Refer to the [Data Model API reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. +In the route handler, you resolve the User Module's main service, then use it to retrieve the logged-in admin user. # Event Data Payload @@ -7586,507 +7227,231 @@ This logs the product ID received in the `product.created` event’s data payloa Refer to [this reference](!resources!/events-reference) for a full list of events emitted by Medusa and their data payloads. */} -# Write Migration +# Add Data Model Check Constraints -In this chapter, you'll learn how to create a migration and write it manually. +In this chapter, you'll learn how to add check constraints to your data model. -## What is a Migration? +## What is a Check Constraint? -A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. +For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. *** -## How to Write a Migration? - -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 the specified modules' data models. - -Alternatively, you can manually create a migration file under the `migrations` directory of your module. +## How to Set a Check Constraint? -For example: +To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. -```ts title="src/modules/hello/migrations/Migration20240429.ts" -import { Migration } from "@mikro-orm/migrations" +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: -export class Migration20240702105919 extends Migration { +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" - async up(): Promise { - this.addSql("create table if not exists \"my_custom\" (\"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 \"my_custom_pkey\" primary key (\"id\"));") - } +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + (columns) => `${columns.price} >= 0`, +]) +``` - async down(): Promise { - this.addSql("drop table if exists \"my_custom\" cascade;") - } +The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. -} -``` +The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. -The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. +You can also pass an object to the `checks` method: -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. +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" -In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) +``` -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. +The object accepts the following properties: + +- `name`: The check constraint's name. +- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). *** -## Run the Migration +## Apply in Migrations -To run your migration, run the following command: +After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. -This command also syncs module links. If you don't want that, use the `--skip-links` option. +To generate a migration for the data model's module then reflect it on the database, run the following command: ```bash +npx medusa db:generate custom_module 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 helloModuleService -``` +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. -This rolls back the last ran migration on the Hello Module. -*** +# Configure Data Model Properties -## More Database Commands +In this chapter, you’ll learn how to configure data model properties. -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). +## Property’s Default Value +Use the `default` method on a property's definition to specify the default value of a property. -# Searchable Data Model Property +For example: -In this chapter, you'll learn what a searchable property is and how to define it. +```ts highlights={defaultHighlights} +import { model } from "@medusajs/framework/utils" -## What is a Searchable Property? +const MyCustom = model.define("my_custom", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... +}) -Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. +export default MyCustom +``` -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. *** -## Define a Searchable Property +## Nullable Property -Use the `searchable` method on a `text` property to indicate that it's searchable. +Use the `nullable` method to indicate that a property’s value can be `null`. For example: -```ts highlights={searchableHighlights} +```ts highlights={nullableHighlights} import { model } from "@medusajs/framework/utils" const MyCustom = model.define("my_custom", { - name: model.text().searchable(), + price: model.bigNumber().nullable(), // ... }) export default MyCustom ``` -In this example, the `name` property is searchable. - -### Search Example - -If you pass a `q` filter to the `listMyCustoms` method: - -```ts -const myCustoms = await helloModuleService.listMyCustoms({ - q: "John", -}) -``` - -This retrieves records that include `John` in their `name` property. - - -# Data Model Relationships - -In this chapter, you’ll learn how to define relationships between data models in your module. - -## What is a Relationship Property? - -A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. - -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. - -You want to create a relation between data models in the same module. - -You want to create a relationship between data models in different modules. Use module links instead. - *** -## One-to-One Relationship - -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. - -To define a one-to-one relationship, create relationship properties in the data models using the following methods: +## Unique Property -1. `hasOne`: indicates that the model has one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +The `unique` method indicates that a property’s value must be unique in the database through a unique index. For example: -```ts highlights={oneToOneHighlights} +```ts highlights={uniqueHighlights} import { model } from "@medusajs/framework/utils" const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), + email: model.text().unique(), + // ... }) -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) +export default User ``` -In the example above, a user has one email, and an email belongs to one user. +In this example, multiple users can’t have the same email. -The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. +# Emit Workflow and Service Events -### Optional Relationship +In this chapter, you'll learn about event types and how to emit an event in a service or workflow. -To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). +## Event Types -### One-sided One-to-One Relationship +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. -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. +There are two types of events in Medusa: -For example: +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. -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" +### Which Event Type to Use? -const User = model.define("user", { - id: model.id().primaryKey(), -}) +**Workflow events** are the most common event type in development, as most custom features and customizations are built around workflows. -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), -}) -``` +Some examples of workflow events: -### One-to-One Relationship in the Database +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. -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: +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. -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. +Some examples of service events: -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +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. *** -## One-to-Many Relationship - -A one-to-many relationship indicates that one record of a data model has many records of another data model. - -To define a one-to-many relationship, create relationship properties in the data models using the following methods: +## Emit Event in a Workflow -1. `hasMany`: indicates that the model has more than one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. +To emit a workflow event, use the `emitEventStep` helper step provided in the `@medusajs/medusa/core-flows` package. For example: -```ts highlights={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" +```ts highlights={highlights} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) +const helloWorldWorkflow = createWorkflow( + "hello-world", + () => { + // ... -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) + emitEventStep({ + eventName: "custom.created", + data: { + id: "123", + // other data payload + }, + }) + } +) ``` -In this example, a store has many products, but a product belongs to one store. - -### Optional Relationship +The `emitEventStep` accepts an object having the following properties: -To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). +- `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. -### One-to-Many Relationship in the Database +In this example, you emit the event `custom.created` and pass in the data payload an ID property. -When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: +### Test it Out -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. +If you execute the workflow, the event is emitted and you can see it in your application's logs. -![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) +Any subscribers listening to the event are executed. *** -## Many-to-Many Relationship +## Emit Event in a Service -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +To emit a service event: -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. +1. Resolve `event_bus` from the module's container in your service's constructor: -For example: - -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" - -const Order = model.define("order", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - mappedBy: "orders", - pivotTable: "order_product", - joinColumn: "order_id", - inverseJoinColumn: "product_id", - }), -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) -``` - -The `manyToMany` method accepts two parameters: - -1. A function that returns the associated data model. -2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: - - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. - - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. - - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. - -The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). - -Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. - -In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. - -### Many-to-Many Relationship in the Database - -When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - -The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables. - -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: - -- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; -- Or the inferred name `{table_name}_id`. - -![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) - -### Many-To-Many with Custom Columns - -To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. - -For example: - -```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" - -export const Order = model.define("order_test", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct, - }), -}) - -export const Product = model.define("product_test", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order), -}) - -export const OrderProduct = model.define("orders_products", { - id: model.id().primaryKey(), - order: model.belongsTo(() => Order, { - mappedBy: "products", - }), - product: model.belongsTo(() => Product, { - mappedBy: "orders", - }), - metadata: model.json().nullable(), -}) -``` - -The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. - -The `OrderProduct` model defines, aside from the ID, the following properties: - -- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. -- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. -- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. - -*** - -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - -*** - -## Cascades - -When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. - -For example, if a store is deleted, its products should also be deleted. - -The `cascades` method used on a data model configures which child records an operation is cascaded to. - -For example: - -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" - -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) -``` - -The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. - -In the example above, when a store is deleted, its associated products are also deleted. - - -# 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 +### Extending Service Factory ```ts title="src/modules/hello/service.ts" highlights={["9"]} import { IEventBusService } from "@medusajs/framework/types" @@ -8171,7032 +7536,6506 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Add Columns to a Link +# Data Model Default Properties -In this chapter, you'll learn how to add custom columns to a link definition and manage them. +In this chapter, you'll learn about the properties available by default in your data model. -## How to Add Custom Columns to a Link's Table? +When you create a data model, the following properties are created for you by Medusa: -The `defineLink` function used to define a link accepts a third parameter, which is an object of options. +- `created_at`: A `dateTime` property that stores when a record of the data model was created. +- `updated_at`: A `dateTime` property that stores when a record of the data model was updated. +- `deleted_at`: A `dateTime` property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date. -To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: -```ts highlights={linkHighlights} -import HelloModule from "../modules/hello" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" +# Data Model Database Index -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom, - { - database: { - extraColumns: { - metadata: { - type: "json", - }, - }, - }, - } -) -``` +In this chapter, you’ll learn how to define a database index on a data model. -This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. +## Define Database Index on Property -### Database Options +Use the `index` method on a property's definition to define a database index. -The `database` property defines configuration for the table created in the database. +For example: -Its `extraColumns` property defines custom columns to create in the link's table. +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), +}) -### Column Configurations +export default MyCustom +``` -The column's configurations object accepts the following properties: +The `index` method optionally accepts the name of the index as a parameter. -- `type`: The column's type. Possible values are: - - `string` - - `text` - - `integer` - - `boolean` - - `date` - - `time` - - `datetime` - - `enum` - - `json` - - `array` - - `enumArray` - - `float` - - `double` - - `decimal` - - `bigint` - - `mediumint` - - `smallint` - - `tinyint` - - `blob` - - `uuid` - - `uint8array` -- `defaultValue`: The column's default value. -- `nullable`: Whether the column can have `null` values. +In this example, you define an index on the `name` property. *** -## Set Custom Column when Creating Link +## Define Database Index on Data Model -The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link. +A data model has an `indexes` method that defines database indices on its properties. -For example: +The index can be on multiple columns (composite index). For example: -Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). +```ts highlights={dataModelIndexHighlights} +import { model } from "@medusajs/framework/utils" -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: true, - }, +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], }, -}) -``` +]) -*** +export default MyCustom +``` -## Retrieve Custom Column with Link +The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. -To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). 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. +In the above example, you define a composite index on the `name` and `age` properties. -For example: +### Index Conditions -```ts highlights={retrieveHighlights} -import productHelloLink from "../links/product-hello" +An index can have conditions. For example: -// ... +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" -const { data } = await query.graph({ - entity: productHelloLink.entryPoint, - fields: ["metadata", "product.*", "my_custom.*"], - filters: { - product_id: "prod_123", +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, }, -}) -``` - -This retrieves the product of id `prod_123` and its linked `my_custom` records. - -In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. +]) -*** +export default MyCustom +``` -## Update Custom Column's Value +The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. -Link's `create` method updates a link's data if the link between the specified records already exists. +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. -So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column. +A property's condition can be a negation. For example: -For example: +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" -```ts -await link.create({ - [Modules.PRODUCT]: { - product_id: "123", - }, - HELLO_MODULE: { - my_custom_id: "321", - }, - data: { - metadata: { - test: false, +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, }, }, -}) +]) + +export default MyCustom ``` +A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. -# Query +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. -In this chapter, you’ll learn about Query and how to use it to fetch data from modules. +### Unique Database Index -## What is Query? +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. -Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. +For example: -In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" -*** +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) -## Query Example +export default MyCustom +``` -For example, create the route `src/api/query/route.ts` with the following content: +This creates a unique composite index on the `name` and `age` properties. -```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) +# Manage Relationships - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - }) +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. - res.json({ my_customs: myCustoms }) -} -``` +## Manage One-to-One Relationship -In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. +### BelongsTo Side of One-to-One -Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: +When you create a record of a data model that belongs to another through a one-to-one relation, pass the ID of the other data model's record in the relation property. -- `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. +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set an email's user ID as follows: -The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: +```ts highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user: "123", +}) -```json title="Returned Data" -{ - "data": [ - { - "id": "123", - "name": "test" - } - ] -} +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user: "123", +}) ``` -*** +In the example above, you pass the `user` property when creating or updating an email to specify the user it belongs to. -## Querying the Graph +### HasOne Side -When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. +When you create a record of a data model that has one of another, pass the ID of the other data model's record in the relation property. -This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. +For example, assuming you have the [User and Email data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-one-relationship/index.html.md), set a user's email ID as follows: -*** +```ts highlights={hasOneHighlights} +// when creating a user +const user = await helloModuleService.createUsers({ + // other properties... + email: "123", +}) -## Retrieve Linked Records +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` -Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. -For example: +*** -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "product.*", - ], -}) -``` +## Manage One-to-Many Relationship -`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`. +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. -### Retrieve List Link Records +When you create a record of the data model on the `belongsTo` side, pass the ID of the other data model's record in the `{relation}_id` property, where `{relation}` is the name of the relation property. -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, assuming you have the [Product and Store data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#one-to-many-relationship/index.html.md), set a product's store ID as follows: -For example: +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) -```ts highlights={[["6"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: [ - "id", - "name", - "products.*", - ], +// when updating a product +const product = await helloModuleService.updateProducts({ + id: "321", + // other properties... + store_id: "123", }) ``` -### Apply Filters and Pagination on Linked Records +In the example above, you pass the `store_id` property when creating or updating a product to specify the store it belongs to. -Consider you want to apply filters or pagination configurations on the product(s) linked to `my_custom`. 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. +## Manage Many-to-Many Relationship -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. +If your many-to-many relation is represented with a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship-with-pivotentity) instead. -For example: +### Create Associations -```ts highlights={queryLinkTableHighlights} -import productCustomLink from "../../../links/product-custom" +When you create a record of a data model that has a many-to-many relationship to another data model, pass an array of IDs of the other data model's records in the relation property. -// ... +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), set the association between products and orders as follows: -const { data: productCustoms } = await query.graph({ - entity: productCustomLink.entryPoint, - fields: ["*", "product.*", "my_custom.*"], - pagination: { - take: 5, - skip: 0, - }, +```ts highlights={manyHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + orders: ["123", "321"], +}) + +// when creating an order +const order = await helloModuleService.createOrders({ + id: "321", + // other properties... + products: ["123", "321"], }) ``` -In the object passed to the `graph` method: +In the example above, you pass the `orders` property when you create a product, and you pass the `products` property when you create an order. -- 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 `MyCustom` record. - - `my_custom.*` to retrieve the fields of a `MyCustom` record linked to a product record. +### Update Associations -You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). +When you use the `update` methods generated by the service factory, you also pass an array of IDs as the relation property's value to add new associated records. -The returned `data` is similar to the following: +However, this removes any existing associations to records whose IDs aren't included in the array. -```json title="Example Result" -[{ - "id": "123", - "product_id": "prod_123", - "my_custom_id": "123", - "product": { - "id": "prod_123", - // other product fields... - }, - "my_custom": { - "id": "123", - // other my_custom fields... - } -}] +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you update the product's related orders as so: + +```ts +const product = await helloModuleService.updateProducts({ + id: "123", + // other properties... + orders: ["321"], +}) ``` -*** +If the product was associated with an order, and you don't include that order's ID in the `orders` array, the association between the product and order is removed. -## Apply Filters +So, to add a new association without removing existing ones, retrieve the product first to pass its associated orders when updating the product: -```ts highlights={[["6"], ["7"], ["8"], ["9"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - filters: { - id: [ - "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", - "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", - ], - }, +```ts highlights={updateAssociationHighlights} +const product = await helloModuleService.retrieveProduct( + "123", + { + relations: ["orders"], + } +) + +const updatedProduct = await helloModuleService.updateProducts({ + id: product.id, + // other properties... + orders: [ + ...product.orders.map((order) => order.id), + "321", + ], }) ``` -The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. - -In the example above, you filter the `my_custom` records by multiple IDs. - -Filters don't apply on fields of linked data models from other modules. +This keeps existing associations between the product and orders, and adds a new one. *** -## 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: myCustoms, - metadata: { count, take, skip } = {}, -} = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - skip: 0, - take: 10, - }, -}) -``` +## Manage Many-to-Many Relationship with pivotEntity -The `graph` method's object parameter accepts a `pagination` property to configure the pagination of retrieved records. +If your many-to-many relation is represented without a [pivotEntity](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), refer to [this section](#manage-many-to-many-relationship) instead. -To paginate the returned records, pass the following properties to `pagination`: +If you have a many-to-many relation with a `pivotEntity` specified, make sure to pass the data model representing the pivot table to [MedusaService](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that your module's service extends. -- `skip`: (required to apply pagination) The number of records to skip before fetching the results. -- `take`: The number of records to fetch. +For example, assuming you have the [Order, Product, and OrderProduct models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-with-custom-columns/index.html.md), add `OrderProduct` to `MedusaService`'s object parameter: -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: +```ts highlights={["4"]} +class HelloModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} +``` -- skip: (\`number\`) The number of records skipped. -- take: (\`number\`) The number of records requested to fetch. -- count: (\`number\`) The total number of records. +This will generate Create, Read, Update and Delete (CRUD) methods for the `OrderProduct` data model, which you can use to create relations between orders and products and manage the extra columns in the pivot table. -### Sort Records +For example: -```ts highlights={[["5"], ["6"], ["7"]]} -const { data: myCustoms } = await query.graph({ - entity: "my_custom", - fields: ["id", "name"], - pagination: { - order: { - name: "DESC", - }, +```ts +// create order-product association +const orderProduct = await helloModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, }, }) -``` -Sorting doesn't work on fields of linked data models from other modules. +// update order-product association +const orderProduct = await helloModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) -To sort returned records, pass an `order` property to `pagination`. +// delete order-product association +await helloModuleService.deleteOrderProducts("123") +``` -The `order` property is an object whose keys are property names, and values are either: +Since the `OrderProduct` data model belongs to the `Order` and `Product` data models, you can set its order and product as explained in the [one-to-many relationship section](#manage-one-to-many-relationship) using `order_id` and `product_id`. -- `ASC` to sort records by that property in ascending order. -- `DESC` to sort records by that property in descending order. +Refer to the [service factory reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for a full list of generated methods and their usages. *** -## Request Query Configurations +## Retrieve Records of Relation -For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: +The `list`, `listAndCount`, and `retrieve` methods of a module's main service accept as a second parameter an object of options. -- 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. +To retrieve the records associated with a data model's records through a relationship, pass in the second parameter object a `relations` property whose value is an array of relationship names. -Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. +For example, assuming you have the [Order and Product data models from the previous chapter](https://docs.medusajs.com/learn/fundamentals/data-models/relationships#many-to-many-relationship/index.html.md), you retrieve a product's orders as follows: -### Step 1: Add Middleware +```ts highlights={retrieveHighlights} +const product = await helloModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) +``` -The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. -```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() +# Infer Type of Data Model -export default defineMiddlewares({ - routes: [ - { - matcher: "/customs", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetCustomSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - ], -}) -``` +In this chapter, you'll learn how to infer the type of a data model. -The `validateAndTransformQuery` accepts two parameters: +## How to Infer Type of Data Model? -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`. +Consider you have a `MyCustom` 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. -### Step 2: Use Configurations in API Route +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. -After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. +For example: -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. +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" // relative path to the model -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been depercated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. +export type MyCustom = InferTypeOf +``` -For example, Create the file `src/api/customs/route.ts` with the following content: +`InferTypeOf` accepts as a type argument the type of the data model. -```ts title="src/api/customs/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +Since the `MyCustom` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) +You can now use the `MyCustom` type to reference a data model in other types, such as in workflow inputs or service method outputs: - const { data: myCustoms } = await query.graph({ - entity: "my_custom", - ...req.queryConfig, - }) +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { MyCustom } from "../models/my-custom" - res.json({ my_customs: myCustoms }) +type MyCustom = InferTypeOf + +class HelloModuleService extends MedusaService({ MyCustom }) { + async doSomething(): Promise { + // ... + } } ``` -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 +# Data Model’s Primary Key -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. +In this chapter, you’ll learn how to configure the primary key of a data model. -```json title="Returned Data" -{ - "my_customs": [ - { - "id": "123", - "name": "test" - } - ] -} -``` +## primaryKey Method -Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. +To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. -Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations/index.html.md) and [pagination](https://docs.medusajs.com/api/store#pagination/index.html.md) in the API reference. +For example: +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -# Query Context +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + // ... +}) -In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +export default MyCustom +``` -## What is Query Context? +In the example above, the `id` property is defined as the data model's primary key. -Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context. -For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md). +# Data Model Property Types -*** +In this chapter, you’ll learn about the types of properties in a data model’s schema. -## How to Use Query Context +## id -The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`). +The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers. -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. +For example: -For example, to retrieve posts using Query while passing the user's language as a context: +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }) +const MyCustom = model.define("my_custom", { + id: model.id(), + // ... }) + +export default MyCustom ``` -In this example, you pass in the context a `lang` property whose value is `es`. +*** -Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model. +## text -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: +The `text` method defines a string property. -```ts highlights={highlights2} -import { MedusaContext, MedusaService } from "@medusajs/framework/utils" -import { Context, FindConfig } from "@medusajs/framework/types" -import Post from "./models/post" -import Author from "./models/author" +For example: -class BlogModuleService extends MedusaService({ - Post, - Author -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +```ts highlights={textHighlights} +import { model } from "@medusajs/framework/utils" - let posts = await super.listPosts(filters, config, sharedContext) +const MyCustom = model.define("my_custom", { + name: model.text(), + // ... +}) - if (context.lang === "es") { - posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } +export default MyCustom +``` - return posts - } -} +*** -export default BlogModuleService -``` +## number -In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query. +The `number` method defines a number property. -You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts. +For example: -All posts returned will now have their titles appended with "en español". +```ts highlights={numberHighlights} +import { model } from "@medusajs/framework/utils" -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). +const MyCustom = model.define("my_custom", { + age: model.number(), + // ... +}) + +export default MyCustom +``` *** -## Passing Query Context to Related Data Models +## float -If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). -For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). +The `float` method defines a number property that allows for values with decimal places. -For example, to pass a context for the post's authors: +Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). -```ts highlights={highlights3} -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - author: QueryContext({ - lang: "es", - }) - }) +For example: + +```ts highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + rating: model.float(), + // ... }) + +export default MyCustom ``` -Then, in the `listPosts` method, you can handle the context for the post's authors: +*** -```ts highlights={highlights4} -import { MedusaContext, MedusaService } from "@medusajs/framework/utils" -import { Context, FindConfig } from "@medusajs/framework/types" -import Post from "./models/post" -import Author from "./models/author" +## bigNumber -class BlogModuleService extends MedusaService({ - Post, - Author -}){ - // @ts-ignore - async listPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context +The `bigNumber` method defines a number property that expects large numbers, such as prices. - let posts = await super.listPosts(filters, config, sharedContext) +Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). - const isPostLangEs = context.lang === "es" - const isAuthorLangEs = context.author?.lang === "es" +For example: - if (isPostLangEs || isAuthorLangEs) { - posts = posts.map((post) => { - return { - ...post, - title: isPostLangEs ? post.title + " en español" : post.title, - author: { - ...post.author, - name: isAuthorLangEs ? post.author.name + " en español" : post.author.name, - } - } - }) - } +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" - return posts - } -} +const MyCustom = model.define("my_custom", { + price: model.bigNumber(), + // ... +}) -export default BlogModuleService +export default MyCustom ``` -The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. - *** -## Passing Query Context to Linked Data Models +## boolean -If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model. +The `boolean` method defines a boolean property. -For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so: +For example: -```ts highlights={highlights5} -const { data } = await query.graph({ - entity: "product", - fields: ["*", "post.*"], - context: { - post: QueryContext({ - lang: "es", - }) - } +```ts highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + hasAccount: model.boolean(), + // ... }) + +export default MyCustom ``` -In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. +*** -To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). +### enum +The `enum` method defines a property whose value can only be one of the specified values. -# Link +For example: -In this chapter, you’ll learn what Link is and how to use it to manage links. +```ts highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" -As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. +const MyCustom = model.define("my_custom", { + color: model.enum(["black", "white"]), + // ... +}) -## What is Link? +export default MyCustom +``` -Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. +The `enum` method accepts an array of possible string values. + +*** + +## dateTime + +The `dateTime` method defines a timestamp property. For example: -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" -export async function POST( - req: MedusaRequest, - res: MedusaResponse -): Promise { - const link = req.scope.resolve( - ContainerRegistrationKeys.LINK - ) - +const MyCustom = model.define("my_custom", { + date_of_birth: model.dateTime(), // ... -} -``` +}) -You can use its methods to manage links, such as create or delete links. +export default MyCustom +``` *** -## Create Link +## json -To create a link between records of two data models, use the `create` method of Link. +The `json` method defines a property whose value is a stringified JSON object. For example: -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, +const MyCustom = model.define("my_custom", { + metadata: model.json(), + // ... }) + +export default MyCustom ``` -The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. +*** -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. +## array -The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record. +The `array` method defines an array of strings property. -So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module. +For example: + +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + names: model.array(), + // ... +}) + +export default MyCustom +``` *** -## Dismiss Link +## Properties Reference -To remove a link between records of two data models, use the `dismiss` method of Link. +Refer to the [Data Model API reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. -For example: -```ts -import { Modules } from "@medusajs/framework/utils" +# Write Migration -// ... +In this chapter, you'll learn how to create a migration and write it manually. -await link.dismiss({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - "helloModuleService": { - my_custom_id: "mc_123", - }, -}) -``` +## What is a Migration? -The `dismiss` method accepts the same parameter type as the [create method](#create-link). +A migration is a class created in a TypeScript or JavaScript file under a module's `migrations` directory. It has two methods: -The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. *** -## Cascade Delete Linked Records +## How to Write a Migration? -If a record is deleted, use the `delete` method of Link to delete all linked records. +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 the specified modules' data models. + +Alternatively, you can manually create a migration file under the `migrations` directory of your module. For example: -```ts -import { Modules } from "@medusajs/framework/utils" +```ts title="src/modules/hello/migrations/Migration20240429.ts" +import { Migration } from "@mikro-orm/migrations" -// ... +export class Migration20240702105919 extends Migration { -await productModuleService.deleteVariants([variant.id]) + async up(): Promise { + this.addSql("create table if not exists \"my_custom\" (\"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 \"my_custom_pkey\" primary key (\"id\"));") + } -await link.delete({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) + async down(): Promise { + this.addSql("drop table if exists \"my_custom\" cascade;") + } + +} ``` -This deletes all records linked to the deleted product. +The migration's file name should be of the format `Migration{YEAR}{MONTH}{DAY}.ts`. 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. -## Restore Linked Records +In the example above, the `up` method creates the table `my_custom`, and the `down` method drops the table if the migration is reverted. -If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. -For example: +*** -```ts -import { Modules } from "@medusajs/framework/utils" +## Run the Migration -// ... +To run your migration, run the following command: -await productModuleService.restoreProducts(["prod_123"]) +This command also syncs module links. If you don't want that, use the `--skip-links` option. -await link.restore({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, -}) +```bash +npx medusa db:migrate ``` +This reflects the changes in the database as implemented in the migration's `up` method. -# 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. +## Rollback the Migration -Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts/index.html.md), 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. +To rollback or revert the last migration you ran for a module, run the following command: -So, in this chapter, to extend the create product flow and associate a brand with a product, you will: +```bash +npx medusa db:rollback helloModuleService +``` -- 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`. +This rolls back the last ran migration on the Hello Module. -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 +## More Database Commands -- [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) +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). -*** -## 1. Consume the productCreated Hook +# Searchable Data Model Property -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. +In this chapter, you'll learn what a searchable property is and how to define it. -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). +## What is a Searchable Property? -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/index.html.md) 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. +Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. -To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: +When the `q` filter is passed, the data model's searchable properties are queried to find matching records. -![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" +## Define a Searchable Property -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 `productCreated`, 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. +Use the `searchable` method on a `text` property to indicate that it's searchable. -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). +For example: -To use Link in the `productCreated` hook, replace the `TODO` with the following: +```ts highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" -```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} -const link = container.resolve("link") -const logger = container.resolve("logger") +const MyCustom = model.define("my_custom", { + name: model.text().searchable(), + // ... +}) -const links: LinkDefinition[] = [] +export default MyCustom +``` -for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) -} +In this example, the `name` property is searchable. -await link.create(links) +### Search Example -logger.info("Linked brand to products") +If you pass a `q` filter to the `listMyCustoms` method: -return new StepResponse(links, links) +```ts +const myCustoms = await helloModuleService.listMyCustoms({ + q: "John", +}) ``` -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) +This retrieves records that include `John` in their `name` property. -Finally, you return an instance of `StepResponse` returning the created links. -### Dismiss Links in Compensation +# Data Model Relationships -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. +In this chapter, you’ll learn how to define relationships between data models in your module. -To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: +## What is a Relationship Property? -```ts title="src/workflows/hooks/created-product.ts" -createProductsWorkflow.hooks.productsCreated( - // ... - (async (links, { container }) => { - if (!links?.length) { - return - } +A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. - const link = container.resolve("link") +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. - await link.dismiss(links) - }) -) -``` +You want to create a relation between data models in the same module. -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. +You want to create a relationship between data models in different modules. Use module links instead. *** -## 2. Configure Additional Data Validation +## One-to-One Relationship -Now that you've consumed the `productCreated` 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. +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. -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: +To define a one-to-one relationship, create relationship properties in the data models using the following methods: -![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) +1. `hasOne`: indicates that the model has one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" +For example: -// ... +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), }) ``` -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/). +In the example above, a user has one email, and an email belongs to one user. -So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. +The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. -*** +The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. -## Test it Out +### Optional Relationship -To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: +To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` +### One-sided One-to-One Relationship -Make sure to replace the email and password in the request body with your user's credentials. +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. -Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: +For example: -```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"] - } - ], - "additional_data": { - "brand_id": "{brand_id}" - } -}' -``` +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" -Make sure to replace `{token}` with the token you received from the previous request, and `{brand_id}` with the ID of a brand in your application. +const User = model.define("user", { + id: model.id().primaryKey(), +}) -The request creates a product and returns it. +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), +}) +``` -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. +### One-to-One Relationship in the Database -*** +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: -## Next Steps: Query Linked Brands and Products +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. -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. +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) +*** -# Module Link Direction +## One-to-Many Relationship -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. +A one-to-many relationship indicates that one record of a data model has many records of another data model. -## Link Direction +To define a one-to-many relationship, create relationship properties in the data models using the following methods: -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. +1. `hasMany`: indicates that the model has more than one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. -For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: +For example: -```ts -export default defineLink( - HelloModule.linkable.myCustom, - ProductModule.linkable.product -) -``` +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" -Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.myCustom -) +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) ``` -The above links are two different links that serve different purposes. +In this example, a store has many products, but a product belongs to one store. -*** +### Optional Relationship -## Which Link Direction to Use? +To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/configure-properties#nullable-property/index.html.md). -### Extend Data Models +### One-to-Many Relationship in the Database -If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. +When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: -For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. -```ts -export default defineLink( - ProductModule.linkable.product, - HelloModule.linkable.subtitle -) -``` +![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) -### Associate Data Models +*** -If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. +## Many-to-Many Relationship -For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: - -```ts -export default defineLink( - HelloModule.linkable.post, - ProductModule.linkable.product -) -``` +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. -# Guide: Define Module Link Between Brand and Product +For example: -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. +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" -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. +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) -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. +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) +``` -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. +The `manyToMany` method accepts two parameters: -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +1. A function that returns the associated data model. +2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: + - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. + - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. + - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. + - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. -### Prerequisites +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. -## 1. Define Link +In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. -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. +### Many-to-Many Relationship in the Database -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: +When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. -![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) +The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables. -```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" +The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` +- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; +- Or the inferred name `{table_name}_id`. -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. +![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) -The `defineLink` function accepts two parameters of the same type, which is either: +### Many-To-Many with Custom Columns -- 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. +To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. -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. +For example: -*** +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" -## 2. Sync the Link to the Database +export const Order = model.define("order_test", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + pivotEntity: () => OrderProduct, + }), +}) -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: +export const Product = model.define("product_test", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order), +}) -```bash -npx medusa db:migrate +export const OrderProduct = model.define("orders_products", { + id: model.id().primaryKey(), + order: model.belongsTo(() => Order, { + mappedBy: "products", + }), + product: model.belongsTo(() => Product, { + mappedBy: "orders", + }), + metadata: model.json().nullable(), +}) ``` -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. +The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. +The `OrderProduct` model defines, aside from the ID, the following properties: + +- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. +- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. +- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. *** -## Next Steps: Extend Create Product Flow +## Set Relationship Name in the Other Model -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. +The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. +This is useful if the relationship property’s name is different from that of the associated data model. -# Guide: Query Product's Brands +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. -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. +For example: -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. +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/framework/utils" -### Prerequisites +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) -- [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) +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. *** -## Approach 1: Retrieve Brands in Existing API Routes +## Cascades -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/index.html.md), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid/index.html.md), 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. +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. -Learn more about selecting fields and relations in the [API Reference](https://docs.medusajs.com/api/admin#select-fields-and-relations/index.html.md). +For example, if a store is deleted, its products should also be deleted. -For example, send the following request to retrieve the list of products with their brands: +The `cascades` method used on a data model configures which child records an operation is cascaded to. -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` +For example: -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/index.html.md). +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" -Any product that is linked to a brand will have a `brand` property in its object: +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) -```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 - } -} +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) ``` -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. +The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. -*** +In the example above, when a store is deleted, its associated products are also deleted. -## 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. +# Create a Plugin -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +In this chapter, you'll learn how to create a Medusa plugin and publish it. -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: +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. -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) +## 1. Create a Plugin Project - res.json({ brands }) -} +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 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: +This will create a new Medusa plugin project in the `my-plugin` directory. -- `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. +### Plugin Directory Structure -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. +After the installation is done, the plugin structure will look like this: -### Test it Out +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) -To test the API route out, send a `GET` request to `/admin/brands`: +- `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. -```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/index.html.md). +## 2. Prepare Plugin -This returns the brands in your store with their linked products. For example: +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. -```json title="Example Response" +For example: + +```json title="package.json" { - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] + "name": "@myorg/plugin-name", + // ... } ``` -*** - -## Summary - -By following the examples of the previous chapters, you: +In addition, make sure that the `keywords` field in `package.json` includes the keyword `medusa-plugin` and `medusa-v2`. This helps Medusa list community plugins on the Medusa website: -- 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. +```json title="package.json" +{ + "keywords": [ + "medusa-plugin", + "medusa-v2" + ], + // ... +} +``` *** -## 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. - - -# Architectural Modules +## 3. Publish Plugin Locally for Development and Testing -In this chapter, you’ll learn about architectural modules. +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. -## What is an Architectural Module? +### Publish and Install Local Package -An architectural module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +### Prerequisites -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. +- [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. -## Architectural Module Types +To publish the plugin to the local registry, run the following command in your plugin project: -There are different architectural module types including: +```bash title="Plugin project" +npx medusa plugin:publish +``` -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) +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`. -- 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. +Next, navigate to your Medusa application: -*** +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` -## Architectural Modules List +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. -Refer to the [Architectural Modules reference](https://docs.medusajs.com/resources/architectural-modules/index.html.md) for a list of Medusa’s architectural modules, available modules to install, and how to create an architectural module. +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: +```bash npm2yarn title="Medusa application" +npm install --save-dev yalc +``` -# Module Container +After that, run the following Medusa CLI command to install the plugin: -In this chapter, you'll learn about the module's container and how to resolve resources in that container. +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` -Since modules are isolated, each module has a local container only used by the resources of that module. +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. -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container. +### Register Plugin in Medusa Application -### List of Registered Resources +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. -Find a list of resources or dependencies registered in a module's container in [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +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: {}, + }, + ], +}) +``` -## Resolve Resources +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. -### Services +#### Pass Module Options through Plugin -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +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 highlights={[["4"], ["10"]]} -import { Logger } from "@medusajs/framework/types" +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` -type InjectedDependencies = { - logger: Logger -} +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). -export default class HelloModuleService { - protected logger_: Logger +### Watch Plugin Changes During Development - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger +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. - this.logger_.info("[HelloModuleService]: Hello World!") - } +To do that, run the following command in your plugin project: - // ... -} +```bash title="Plugin project" +npx medusa plugin:develop ``` -### 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. +This command will: -For example: +- 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. -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +### Start Medusa Application -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +You can start your Medusa application's development server to test out your plugin: - logger.info("[helloWorldLoader]: Hello, World!") -} +```bash npm2yarn title="Medusa application" +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. -# Perform Database Operations in a Service +*** -In this chapter, you'll learn how to perform database operations in a module's service. +## 4. Create Customizations in the Plugin -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. +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. -## Run Queries +- [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) -[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. +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). -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). +### Generating Migrations for Modules -So, to run database queries in a service: +During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: -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` +```bash title="Plugin project" +npx medusa plugin:db:generate +``` -For example, in your service, add the following methods: +This command generates migrations for all modules in the plugin. You can then run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: -```ts highlights={methodsHighlight} -// other imports... -import { - InjectManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { SqlEntityManager } from "@mikro-orm/knex" +```bash title="Medusa application" +npx medusa db:migrate +``` -class HelloModuleService { - // ... +### Importing Module Resources - @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) +Your plugin project should have the following exports in `package.json`: + +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./*": "./.medusa/server/src/*.js" } } ``` -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). +Aside from the `./package.json` and `./providers`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. -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. +The plugin exports the following files and directories: -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +- `./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`. +- `./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 allows you to register the plugin's providers in the Medusa application. +- `./*`: Any other files in the plugin's `src` directory. -*** +With these exports, you can import the plugin's resources in the Medusa application's code like this: -## Execute Operations in Transactions +`@myorg/plugin-name` is the plugin package's name. -To wrap database operations in a transaction, you create two methods: +```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" +``` -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. +And you can register a module provider in the Medusa application's `medusa-config.ts` like this: -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 HelloModuleService { +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ // ... - @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, + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], }, - { - 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 `HelloModuleService` has two methods: +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. -- A protected `update_` that performs the database operations inside a transaction. -- A public `update` that executes the transactional protected method. +### Create Module Providers -The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. +To learn how to create module providers, refer to the following guides: -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +- [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) -### Why Wrap a Transactional Method +*** -The variables in the transactional method (for example, `update_`) hold values that are uncommitted to the database. They're only committed once the method finishes execution. +## 5. Publish Plugin to NPM -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. +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: -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. +```bash +npx medusa plugin:build +``` -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. +The command will compile an output in the `.medusa/server` directory. -For example, the `update` method could be changed to the following: +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: -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +```bash +npm publish +``` -class HelloModuleService { - // ... - @InjectManager() - async update( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ) { - const newData = await this.update_(input, sharedContext) +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. - await sendNewDataToSystem(newData) +### Install Public Plugin in Medusa Application - return newData - } -} +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name ``` -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. +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. -### Using Methods in Transactional Methods +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). -If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. +*** -For example: +## Update a Published Plugin -```ts -// other imports... -import { EntityManager } from "@mikro-orm/knex" +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). -class HelloModuleService { - // ... - @InjectTransactionManager() - protected async anotherMethod( - @MedusaContext() sharedContext?: Context - ) { - // ... - } - - @InjectTransactionManager() - protected async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - this.anotherMethod(sharedContext) - } -} -``` +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. -You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. +First, run the following command to change the version of the plugin: -The `anotherMethod` now runs in the same transaction as the `update_` method. +```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. -## Configure Transactions +Then, re-run the same commands for publishing a plugin: -To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container. +```bash +npx medusa plugin:build +npm publish +``` -The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. +This will publish an updated version of your plugin under a new version. -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: +# Add Columns to a Link -### Extending Service Factory +In this chapter, you'll learn how to add custom columns to a link definition and manage them. -```ts highlights={[["14"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" -import { DAL } from "@medusajs/framework/types" +## How to Add Custom Columns to a Link's Table? -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +The `defineLink` function used to define a link accepts a third parameter, which is an object of options. -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected baseRepository_: DAL.RepositoryService +To add custom columns to a link's table, pass in the third parameter of `defineLink` a `database` property: - constructor({ baseRepository }: InjectedDependencies) { - super(...arguments) - this.baseRepository_ = baseRepository - } -} +```ts highlights={linkHighlights} +import HelloModule from "../modules/hello" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" -export default HelloModuleService +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom, + { + database: { + extraColumns: { + metadata: { + type: "json", + }, + }, + }, + } +) ``` -### Without Service Factory - -```ts highlights={[["10"]]} -import { DAL } from "@medusajs/framework/types" +This adds to the table created for the link between `product` and `myCustom` a `metadata` column of type `json`. -type InjectedDependencies = { - baseRepository: DAL.RepositoryService -} +### Database Options -class HelloModuleService { - protected baseRepository_: DAL.RepositoryService +The `database` property defines configuration for the table created in the database. - constructor({ baseRepository }: InjectedDependencies) { - this.baseRepository_ = baseRepository - } -} +Its `extraColumns` property defines custom columns to create in the link's table. -export default HelloModuleService -``` +`extraColumns`'s value is an object whose keys are the names of the columns, and values are the column's configurations as an object. -Then, add the following method that uses it: +### Column Configurations -```ts highlights={repoHighlights} -// ... -import { - InjectManager, - InjectTransactionManager, - MedusaContext, -} from "@medusajs/framework/utils" -import { Context } from "@medusajs/framework/types" -import { EntityManager } from "@mikro-orm/knex" +The column's configurations object accepts the following properties: -class HelloModuleService { - // ... - @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, - } - ) +- `type`: The column's type. Possible values are: + - `string` + - `text` + - `integer` + - `boolean` + - `date` + - `time` + - `datetime` + - `enum` + - `json` + - `array` + - `enumArray` + - `float` + - `double` + - `decimal` + - `bigint` + - `mediumint` + - `smallint` + - `tinyint` + - `blob` + - `uuid` + - `uint8array` +- `defaultValue`: The column's default value. +- `nullable`: Whether the column can have `null` values. - // retrieve again - const updatedRecord = await transactionManager.execute( - `SELECT * FROM my_custom WHERE id = '${input.id}'` - ) +*** - return updatedRecord - }, - { - transaction: sharedContext.transactionManager, - } - ) - } +## Set Custom Column when Creating Link - @InjectManager() - async update( - input: { - id: string, - name: string +The object you pass to Link's `create` method accepts a `data` property. Its value is an object whose keys are custom column names, and values are the value of the custom column for this link. + +For example: + +Learn more about Link, how to resolve it, and its methods in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). + +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: true, }, - @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. +## Retrieve Custom Column with Link -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. +To retrieve linked records with their custom columns, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). 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. -Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. +For example: -### Transaction Options +```ts highlights={retrieveHighlights} +import productHelloLink from "../links/product-hello" -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. +const { data } = await query.graph({ + entity: productHelloLink.entryPoint, + fields: ["metadata", "product.*", "my_custom.*"], + filters: { + product_id: "prod_123", + }, +}) +``` -```ts highlights={[["16"]]} -// other imports... -import { EntityManager } from "@mikro-orm/knex" +This retrieves the product of id `prod_123` and its linked `my_custom` records. -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - transaction: sharedContext.transactionManager, - } - ) - } -} -``` +In the `fields` array you pass `metadata`, which is the custom column to retrieve of the link. -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={[["19"]]} -// other imports... -import { IsolationLevel } from "@mikro-orm/core" +## Update Custom Column's Value -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string - }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - isolationLevel: IsolationLevel.READ_COMMITTED, - } - ) - } -} -``` +Link's `create` method updates a link's data if the link between the specified records already exists. -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. +So, to update the value of a custom column in a created link, use the `create` method again passing it a new value for the custom column. -```ts highlights={[["16"]]} -class HelloModuleService { - // ... - @InjectTransactionManager() - async update_( - input: { - id: string, - name: string +For example: + +```ts +await link.create({ + [Modules.PRODUCT]: { + product_id: "123", + }, + HELLO_MODULE: { + my_custom_id: "321", + }, + data: { + metadata: { + test: false, }, - @MedusaContext() sharedContext?: Context - ): Promise { - return await this.baseRepository_.transaction( - async (transactionManager) => { - // ... - }, - { - enableNestedTransactions: false, - } - ) - } -} + }, +}) ``` -# Loaders - -In this chapter, you’ll learn about loaders and how to use them. - -## What is a Loader? +# Module Link Direction -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 this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. -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. +## Link Direction -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. +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. -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). +For example, the following defines a link from the `helloModuleService`'s `myCustom` data model to the Product Module's `product` data model: -*** +```ts +export default defineLink( + HelloModule.linkable.myCustom, + ProductModule.linkable.product +) +``` -## How to Create a Loader? +Whereas the following defines a link from the Product Module's `product` data model to the `helloModuleService`'s `myCustom` data model: -### 1. Implement Loader Function +```ts +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.myCustom +) +``` -You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. +The above links are two different links that serve different purposes. -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) +## Which Link Direction to Use? -Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +### Extend Data Models -```ts title="src/modules/hello/loaders/hello-world.ts" -import { - LoaderOptions, -} from "@medusajs/framework/types" +If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve("logger") +For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: - logger.info("[helloWorldLoader]: Hello, World!") -} +```ts +export default defineLink( + ProductModule.linkable.product, + HelloModule.linkable.subtitle +) ``` -The loader file exports an async function, which is the function executed when the application loads. +### Associate Data Models -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). +If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. -### 2. Export Loader in Module Definition +For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: -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. +```ts +export default defineLink( + HelloModule.linkable.post, + ProductModule.linkable.product +) +``` -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" +# Link -export default Module("hello", { - // ... - loaders: [helloWorldLoader], -}) -``` +In this chapter, you’ll learn what Link is and how to use it to manage links. -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. +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), Remote Link has been deprecated in favor of Link. They have the same usage, so you only need to change the key used to resolve the tool from the Medusa container as explained below. -### Test the Loader +## What is Link? -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: +Link is a class with utility methods to manage links between data models. It’s registered in the Medusa container under the `link` registration name. -```bash npm2yarn -npm run dev -``` +For example: -Then, you'll find the following message logged in the terminal: +```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -```plain -info: [HELLO MODULE] Just started the Medusa application! +export async function POST( + req: MedusaRequest, + res: MedusaResponse +): Promise { + const link = req.scope.resolve( + ContainerRegistrationKeys.LINK + ) + + // ... +} ``` -This indicates that the loader in the `hello` module ran and logged this message. +You can use its methods to manage links, such as create or delete links. *** -## Example: Register Custom MongoDB Connection +## Create Link -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. +To create a link between records of two data models, use the `create` method of Link. -Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. +For example: -### Prerequisites +```ts +import { Modules } from "@medusajs/framework/utils" -- [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: +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) +``` -```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} -import { LoaderOptions } from "@medusajs/framework/types" -import { asValue } from "awilix" -import { MongoClient } from "mongodb" +The `create` method accepts as a parameter an object. The object’s keys are the names of the linked modules. -type ModuleOptions = { - connection_url?: string - db_name?: string -} +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. -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) +The value of each module’s property is an object, whose keys are of the format `{data_model_snake_name}_id`, and values are the IDs of the linked record. - logger.info("Connected to MongoDB") +So, in the example above, you link a record of the `MyCustom` data model in a `hello` module to a `Product` record in the Product Module. - 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: +## Dismiss Link -```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, - }, - }, - ], -}) -``` +To remove a link between records of two data models, use the `dismiss` method of Link. -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: +For example: -- `connection_url`: the URL to connect to the MongoDB database. -- `db_name`: The name of the database to connect to. +```ts +import { Modules } from "@medusajs/framework/utils" -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: +await link.dismiss({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + "helloModuleService": { + my_custom_id: "mc_123", + }, +}) +``` -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. +The `dismiss` method accepts the same parameter type as the [create method](#create-link). -### Use Custom Registered Resource in Module's Service +The keys (names of linked modules) must be in the same [direction](https://docs.medusajs.com/learn/fundamentals/module-links/directions/index.html.md) of the link definition. -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: +## Cascade Delete Linked Records -```ts title="src/modules/mongo/service.ts" -import type { Db } from "mongodb" +If a record is deleted, use the `delete` method of Link to delete all linked records. -type InjectedDependencies = { - mongoClient: Db -} +For example: -export default class MongoModuleService { - private mongoClient_: Db +```ts +import { Modules } from "@medusajs/framework/utils" - constructor({ mongoClient }: InjectedDependencies) { - this.mongoClient_ = mongoClient - } +// ... - async createMovie({ title }: { - title: string - }) { - const moviesCol = this.mongoClient_.collection("movie") +await productModuleService.deleteVariants([variant.id]) - const insertedMovie = await moviesCol.insertOne({ - title, - }) +await link.delete({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, +}) +``` - const movie = await moviesCol.findOne({ - _id: insertedMovie.insertedId, - }) +This deletes all records linked to the deleted product. - return movie - } +*** - async deleteMovie(id: string) { - const moviesCol = this.mongoClient_.collection("movie") +## Restore Linked Records - await moviesCol.deleteOne({ - _id: { - equals: id, - }, - }) - } -} -``` +If a record that was previously soft-deleted is now restored, use the `restore` method of Link to restore all linked records. -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. +For example: -Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: +```ts +import { Modules } from "@medusajs/framework/utils" -```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" +await productModuleService.restoreProducts(["prod_123"]) -export default Module(MONGO_MODULE, { - service: MongoModuleService, - loaders: [mongoConnectionLoader], +await link.restore({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, }) ``` -### 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. - -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. +# Query -- 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. +In this chapter, you’ll learn about Query and how to use it to fetch data from modules. -## How are Modules Isolated? +## What is Query? -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. +Query fetches data across modules. It’s a set of methods registered in the Medusa container under the `query` key. -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. +In your resources, such as API routes or workflows, you can resolve Query to fetch data across custom modules and Medusa’s commerce modules. *** -## Why are Modules Isolated +## Query Example -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. +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" -## How to Extend Data Model of Another Module? +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -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). + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + }) -*** + res.json({ my_customs: myCustoms }) +} +``` -## How to Use Services of Other Modules? +In the above example, you resolve Query from the Medusa container using the `ContainerRegistrationKeys.QUERY` (`query`) key. -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. +Then, you run a query using its `graph` method. This method accepts as a parameter an object with the following required properties: -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. +- `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. -### Example +The method returns an object that has a `data` property, which holds an array of the retrieved data. For example: -For example, consider you have two modules: +```json title="Returned Data" +{ + "data": [ + { + "id": "123", + "name": "test" + } + ] +} +``` -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: +## Querying the Graph -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brandModuleService" - ) +When you use the `query.graph` method, you're running a query through an internal graph that the Medusa application creates. - const brands = await brandModuleService.listBrands() +This graph collects data models of all modules in your application, including commerce and custom modules, and identifies relations and links between them. - return new StepResponse(brands) - } -) +*** -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +## Retrieve Linked Records - const cmsBrands = await cmsModuleService.createBrands(brands) +Retrieve the records of a linked data model by passing in `fields` the data model's name suffixed with `.*`. - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cmsModuleService" - ) +For example: - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "product.*", + ], +}) ``` -The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. +`.*` means that all of data model's properties should be retrieved. To retrieve a specific property, replace the `*` with the property's name. For example, `product.title`. -Then, create the following workflow that uses these steps: +### Retrieve List Link Records -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() +If the linked data model has `isList` enabled in the link definition, pass in `fields` the data model's plural name suffixed with `.*`. - createBrandsInCmsStep({ brands }) - } -) +For example: + +```ts highlights={[["6"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: [ + "id", + "name", + "products.*", + ], +}) ``` -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. +### Apply Filters and Pagination on Linked Records +Consider you want to apply filters or pagination configurations on the product(s) linked to `my_custom`. To do that, you must query the module link's table instead. -# Commerce Modules +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. -In this chapter, you'll learn about Medusa's commerce modules. +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. -## What is a Commerce Module? +For example: -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. +```ts highlights={queryLinkTableHighlights} +import productCustomLink from "../../../links/product-custom" -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/index.html.md). 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) +const { data: productCustoms } = await query.graph({ + entity: productCustomLink.entryPoint, + fields: ["*", "product.*", "my_custom.*"], + pagination: { + take: 5, + skip: 0, + }, +}) +``` -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. +In the object passed to the `graph` method: -### List of Medusa's Commerce Modules +- 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 `MyCustom` record. + - `my_custom.*` to retrieve the fields of a `MyCustom` record linked to a product record. -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. +You can then apply any [filters](#apply-filters) or [pagination configurations](#apply-pagination). + +The returned `data` is similar to the following: + +```json title="Example Result" +[{ + "id": "123", + "product_id": "prod_123", + "my_custom_id": "123", + "product": { + "id": "prod_123", + // other product fields... + }, + "my_custom": { + "id": "123", + // other my_custom fields... + } +}] +``` *** -## Use Commerce Modules in Custom Flows +## Apply Filters -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. +```ts highlights={[["6"], ["7"], ["8"], ["9"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + filters: { + id: [ + "mc_01HWSVWR4D2XVPQ06DQ8X9K7AX", + "mc_01HWSVWK3KYHKQEE6QGS2JC3FX", + ], + }, +}) +``` -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: +The `query.graph` function accepts a `filters` property. You can use this property to filter retrieved records. -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +In the example above, you filter the `my_custom` records by multiple IDs. -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") +Filters don't apply on fields of linked data models from other modules. - const [,count] = await productModuleService.listAndCountProducts() +*** - return new StepResponse(count) - } -) +## 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: myCustoms, + metadata: { count, take, skip } = {}, +} = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + skip: 0, + take: 10, + }, +}) ``` -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. +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`: -# Modules Directory Structure +- `skip`: (required to apply pagination) The number of records to skip before fetching the results. +- `take`: The number of records to fetch. -In this document, you'll learn about the expected files and directories in your module. +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: -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) +- skip: (\`number\`) The number of records skipped. +- take: (\`number\`) The number of records requested to fetch. +- count: (\`number\`) The total number of records. -## index.ts +### Sort Records -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). +```ts highlights={[["5"], ["6"], ["7"]]} +const { data: myCustoms } = await query.graph({ + entity: "my_custom", + fields: ["id", "name"], + pagination: { + order: { + name: "DESC", + }, + }, +}) +``` -*** +Sorting doesn't work on fields of linked data models from other modules. -## service.ts +To sort returned records, pass an `order` property to `pagination`. -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). +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. *** -## Other Directories +## Request Query Configurations -The following directories are optional and their content are explained more in the following chapters: +For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: -- `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. - - -# Multiple Services in a Module +- 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. -In this chapter, you'll learn how to use multiple services in a module. +Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. -## Module's Main and Internal Services +### Step 1: Add Middleware -A module has one main service only, which is the service exported in the module's definition. +The first step is to use the `validateAndTransformQuery` middleware on the `GET` route. You add the middleware in `src/api/middlewares.ts`: -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. +```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() -## How to Add an Internal Service +export default defineMiddlewares({ + routes: [ + { + matcher: "/customs", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetCustomSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + ], +}) +``` -### 1. Create Service +The `validateAndTransformQuery` accepts two parameters: -To add an internal service, create it in the `services` directory of your module. +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`. -For example, create the file `src/modules/hello/services/client.ts` with the following content: +### Step 2: Use Configurations in API Route -```ts title="src/modules/hello/services/client.ts" -export class ClientService { - async getMessage(): Promise { - return "Hello, World!" - } -} -``` +After applying this middleware, your API route now accepts the `fields`, `offset`, `limit`, and `order` query parameters mentioned above. -### 2. Export Service in Index +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. -Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. +As of [Medusa v2.2.0](https://github.com/medusajs/medusa/releases/tag/v2.2.0), `remoteQueryConfig` has been depercated in favor of `queryConfig`. Their usage is still the same, only the property name has changed. -For example, create the file `src/modules/hello/services/index.ts` with the following content: +For example, Create the file `src/api/customs/route.ts` with the following content: -```ts title="src/modules/hello/services/index.ts" -export * from "./client" -``` +```ts title="src/api/customs/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" -This exports the `ClientService`. +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) -### 3. Resolve Internal Service + const { data: myCustoms } = await query.graph({ + entity: "my_custom", + ...req.queryConfig, + }) -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. + res.json({ my_customs: myCustoms }) +} +``` -For example, in your main service: +This adds a `GET` API route at `/customs`, which is the API route you added the middleware for. -```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} -// other imports... -import { ClientService } from "./services" +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. -type InjectedDependencies = { - clientService: ClientService -} +### Test it Out -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected clientService_: ClientService +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. - constructor({ clientService }: InjectedDependencies) { - super(...arguments) - this.clientService_ = clientService - } +```json title="Returned Data" +{ + "my_customs": [ + { + "id": "123", + "name": "test" + } + ] } ``` -You can now use your internal service in your main service. - -*** +Try passing one of the Query configuration parameters, like `fields` or `limit`, and you'll see its impact on the returned result. -## Resolve Resources in Internal Service +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. -Resolve dependencies from your module's container in the constructor of your internal service. -For example: +# Query Context -```ts -import { Logger } from "@medusajs/framework/types" +In this chapter, you'll learn how to pass contexts when retrieving data with [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). -type InjectedDependencies = { - logger: Logger -} +## What is Query Context? -export class ClientService { - protected logger_: Logger +Query context is a way to pass additional information when retrieving data with Query. This data can be useful when applying custom transformations to the retrieved data based on the current context. - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - } -} -``` +For example, consider you have a Blog Module with posts and authors. You can accept the user's language as a context and return the posts in the user's language. Another example is how Medusa uses Query Context to [retrieve product variants' prices based on the customer's currency](https://docs.medusajs.com/resources/commerce-modules/product/guides/price/index.html.md). *** -## Access Module Options +## How to Use Query Context -Your internal service can't access the module's options. +The `query.graph` method accepts an optional `context` parameter that can be used to pass additional context either to the data model you're retrieving (for example, `post`), or its related and linked models (for example, `author`). -To retrieve the module's options, use the `configModule` registered in the module's container, which is the configurations in `medusa-config.ts`. +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -For example: +For example, to retrieve posts using Query while passing the user's language as a context: ```ts -import { ConfigModule } from "@medusajs/framework/types" -import { HELLO_MODULE } from ".." +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }) +}) +``` -export type InjectedDependencies = { - configModule: ConfigModule -} +In this example, you pass in the context a `lang` property whose value is `es`. -export class ClientService { - protected options: Record +Then, to handle the context while retrieving records of the data model, in the associated module's service you override the generated `list` method of the data model. - constructor({ configModule }: InjectedDependencies) { - const moduleDef = configModule.modules[HELLO_MODULE] +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: - if (typeof moduleDef !== "boolean") { - this.options = moduleDef.options +```ts highlights={highlights2} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" + +class BlogModuleService extends MedusaService({ + Post, + Author +}){ + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + let posts = await super.listPosts(filters, config, sharedContext) + + if (context.lang === "es") { + posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) } + + return posts } } + +export default BlogModuleService ``` -The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. +In the above example, you override the generated `listPosts` method. This method receives as a first parameter the filters passed to the query, but it also includes a `context` property that holds the context passed to the query. -If its value is not a `boolean`, set the service's options to the module configuration's `options` property. +You extract the context from `filters`, then retrieve the posts using the parent's `listPosts` method. After that, if the language is set in the context, you transform the titles of the posts. +All posts returned will now have their titles appended with "en español". -# Service Constraints +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). -This chapter lists constraints to keep in mind when creating a service. +*** -## Use Async Methods +## Passing Query Context to Related Data Models -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. +If you're retrieving a data model and you want to pass context to its associated model in the same module, you can pass them as part of `QueryContext`'s parameter, then handle them in the same `list` method. -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: +For linked data models, check out the [next section](#passing-query-context-to-linked-data-models). -```ts -await helloModuleService.getMessage() +For example, to pass a context for the post's authors: + +```ts highlights={highlights3} +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + author: QueryContext({ + lang: "es", + }) + }) +}) ``` -So, make sure your service's methods are always async to avoid unexpected errors or behavior. +Then, in the `listPosts` method, you can handle the context for the post's authors: -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +```ts highlights={highlights4} +import { MedusaContext, MedusaService } from "@medusajs/framework/utils" +import { Context, FindConfig } from "@medusajs/framework/types" +import Post from "./models/post" +import Author from "./models/author" -class HelloModuleService extends MedusaService({ - MyCustom, +class BlogModuleService extends MedusaService({ + Post, + Author }){ - // Don't - getMessage(): string { - return "Hello, World!" - } + // @ts-ignore + async listPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context - // Do - async getMessage(): Promise { - return "Hello, World!" + let posts = await super.listPosts(filters, config, sharedContext) + + const isPostLangEs = context.lang === "es" + const isAuthorLangEs = context.author?.lang === "es" + + if (isPostLangEs || isAuthorLangEs) { + posts = posts.map((post) => { + return { + ...post, + title: isPostLangEs ? post.title + " en español" : post.title, + author: { + ...post.author, + name: isAuthorLangEs ? post.author.name + " en español" : post.author.name, + } + } + }) + } + + return posts } } -export default HelloModuleService +export default BlogModuleService ``` +The context in `filters` will also have the context for `author`, which you can use to make transformations to the post's authors. -# 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. +## Passing Query Context to Linked Data Models -## What are Module Options? +If you're retrieving a data model and you want to pass context to a linked model in a different module, pass to the `context` property an object instead, where its keys are the linked model's name and the values are the context for that linked model. -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. +For example, consider the Product Module's `Product` data model is linked to the Blog Module's `Post` data model. You can pass context to the `Post` data model while retrieving products like so: + +```ts highlights={highlights5} +const { data } = await query.graph({ + entity: "product", + fields: ["*", "post.*"], + context: { + post: QueryContext({ + lang: "es", + }) + } +}) +``` + +In this example, you retrieve products and their associated posts. You also pass a context for `post`, indicating the customer's language. + +To handle the context, you override the generated `listPosts` method of the Blog Module as explained [previously](#how-to-use-query-context). + + +# Architectural Modules + +In this chapter, you’ll learn about architectural modules. + +## What is an Architectural Module? + +An architectural 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. *** -## How to Pass Options to a Module? +## Architectural Module Types -To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. +There are different architectural module types including: -For example: +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/hello", - options: { - capitalize: true, - }, - }, - ], -}) -``` +- 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. -The `options` property’s value is an object. You can pass any properties you want. +*** -### Pass Options to a Module in a Plugin +## Architectural Modules List -If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. +Refer to the [Architectural Modules reference](https://docs.medusajs.com/resources/architectural-modules/index.html.md) for a list of Medusa’s architectural modules, available modules to install, and how to create an architectural module. -For example: -```ts title="medusa-config.ts" -import { defineConfig } from "@medusajs/framework/utils" -module.exports = defineConfig({ - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - capitalize: true, - }, - }, - ], -}) -``` +# Module Container -The `options` property in the plugin configuration is passed to all modules in a plugin. +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 [this Development Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). *** -## Access Module Options in Main Service +## Resolve Resources -The module’s main service receives the module options as a second parameter. +### 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 title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean +type InjectedDependencies = { + logger: Logger } -export default class HelloModuleService extends MedusaService({ - MyCustom, -}){ - protected options_: ModuleOptions +export default class HelloModuleService { + protected logger_: Logger - constructor({}, options?: ModuleOptions) { - super(...arguments) + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger - this.options_ = options || { - capitalize: false, - } + this.logger_.info("[HelloModuleService]: Hello World!") } // ... } ``` -*** - -## Access Module Options in Loader +### Loader -The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. +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 title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} +```ts highlights={[["9"]]} import { LoaderOptions, } from "@medusajs/framework/types" - -// recommended to define type in another file -type ModuleOptions = { - capitalize?: boolean -} +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" export default async function helloWorldLoader({ - options, -}: LoaderOptions) { - - console.log( - "[HELLO MODULE] Just started the Medusa application!", - options - ) + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("[helloWorldLoader]: Hello, World!") } ``` -# Create a Plugin - -In this chapter, you'll learn how to create a Medusa plugin and publish it. +# Commerce Modules -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. +In this chapter, you'll learn about Medusa's commerce modules. -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +## What is a Commerce Module? -## 1. Create a Plugin Project +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. -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 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. -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: +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) -```bash -npx create-medusa-app my-plugin --plugin -``` +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. -This will create a new Medusa plugin project in the `my-plugin` directory. +### List of Medusa's Commerce Modules -### Plugin Directory Structure +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of commerce modules in Medusa. -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) +## Use Commerce Modules in Custom Flows -- `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. +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: -## 2. Prepare Plugin +```ts highlights={highlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -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. +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") -For example: + const [,count] = await productModuleService.listAndCountProducts() -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} + return new StepResponse(count) + } +) ``` -In addition, make sure that the `keywords` field in `package.json` includes the keyword `medusa-plugin` and `medusa-v2`. This helps Medusa list community plugins on the Medusa website: +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. -```json title="package.json" -{ - "keywords": [ - "medusa-plugin", - "medusa-v2" - ], - // ... -} -``` -*** - -## 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 +# Perform Database Operations in a Service -### Prerequisites +In this chapter, you'll learn how to perform database operations in a module's service. -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) +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. -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. +## Run Queries -To publish the plugin to the local registry, run the following command in your plugin project: +[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. -```bash title="Plugin project" -npx medusa plugin:publish -``` +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). -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`. +So, to run database queries in a service: -Next, navigate to your Medusa application: +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` -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` +For example, in your service, add the following methods: -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. +```ts highlights={methodsHighlight} +// other imports... +import { + InjectManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { SqlEntityManager } from "@mikro-orm/knex" -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: +class HelloModuleService { + // ... -```bash npm2yarn title="Medusa application" -npm install --save-dev yalc + @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) + } +} ``` -After that, run the following Medusa CLI command to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` +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. -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. +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). -### Register Plugin in Medusa Application +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. -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -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: {}, - }, - ], -}) -``` +## Execute Operations in Transactions -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. +To wrap database operations in a transaction, you create two methods: -#### Pass Module Options through Plugin +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. -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. +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 title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ +```ts highlights={opHighlights} +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string }, - ], -}) -``` - -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 + @MedusaContext() sharedContext?: Context + ): Promise { + const transactionManager = sharedContext.transactionManager + await transactionManager.nativeUpdate( + "my_custom", + { + id: input.id, + }, + { + name: input.name, + } + ) -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. + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) -To do that, run the following command in your plugin project: + return updatedRecord + } -```bash title="Plugin project" -npx medusa plugin:develop + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} ``` -This command will: +The `HelloModuleService` has two methods: -- 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. +- A protected `update_` that performs the database operations inside a transaction. +- A public `update` that executes the transactional protected method. -### Start Medusa Application +The shared context's `transactionManager` property holds the transactional entity manager (injected by `InjectTransactionManager`) that you use to perform database operations. -You can start your Medusa application's development server to test out your plugin: +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -```bash npm2yarn title="Medusa application" -npm run dev -``` +### Why Wrap a Transactional Method -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. +The variables in the transactional method (for example, `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. -## 4. Create Customizations in the Plugin +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. -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. +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. -- [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) +For example, the `update` method could be changed to the following: -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). +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" -### Generating Migrations for Modules +class HelloModuleService { + // ... + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + const newData = await this.update_(input, sharedContext) -During your development, you may need to generate migrations for modules in your plugin. To do that, use the `plugin:db:generate` command: + await sendNewDataToSystem(newData) -```bash title="Plugin project" -npx medusa plugin:db:generate + return newData + } +} ``` -This command generates migrations for all modules in the plugin. You can then run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: +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. -```bash title="Medusa application" -npx medusa db:migrate -``` +### Using Methods in Transactional Methods -### Importing Module Resources +If your transactional method uses other methods that accept a Medusa context, pass the shared context to those methods. -Your plugin project should have the following exports in `package.json`: +For example: -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./*": "./.medusa/server/src/*.js" +```ts +// other imports... +import { EntityManager } from "@mikro-orm/knex" + +class HelloModuleService { + // ... + @InjectTransactionManager() + protected async anotherMethod( + @MedusaContext() sharedContext?: Context + ) { + // ... + } + + @InjectTransactionManager() + protected async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + this.anotherMethod(sharedContext) } } ``` -Aside from the `./package.json` and `./providers`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. +You use the `anotherMethod` transactional method in the `update_` transactional method, so you pass it the shared context. -The plugin exports the following files and directories: +The `anotherMethod` now runs in the same transaction as the `update_` method. -- `./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`. -- `./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 allows you to register the plugin's providers in the Medusa application. -- `./*`: Any other files in the plugin's `src` directory. - -With these exports, you can import the plugin's resources in the Medusa application's code like this: - -`@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" -``` +## Configure Transactions -And you can register a module provider in the Medusa application's `medusa-config.ts` like this: +To configure the transaction, such as its [isolation level](https://www.postgresql.org/docs/current/transaction-iso.html), use the `baseRepository` dependency registered in your module's container. -```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... - }, - }, - ], - }, - }, - ], -}) -``` +The `baseRepository` is an instance of a repository class that provides methods to create transactions, run database operations, and more. -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. +The `baseRepository` has a `transaction` method that allows you to run a function within a transaction and configure that transaction. -### Create Module Providers +For example, resolve the `baseRepository` in your service's constructor: -To learn how to create module providers, refer to the following guides: +### Extending Service Factory -- [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) +```ts highlights={[["14"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" +import { DAL } from "@medusajs/framework/types" -*** +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -## 5. Publish Plugin to NPM +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected baseRepository_: DAL.RepositoryService -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: + constructor({ baseRepository }: InjectedDependencies) { + super(...arguments) + this.baseRepository_ = baseRepository + } +} -```bash -npx medusa plugin:build +export default HelloModuleService ``` -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: +### Without Service Factory -```bash -npm publish -``` +```ts highlights={[["10"]]} +import { DAL } from "@medusajs/framework/types" -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. +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} -### Install Public Plugin in Medusa Application +class HelloModuleService { + protected baseRepository_: DAL.RepositoryService -You install a plugin that's published publicly using your package manager. For example: + constructor({ baseRepository }: InjectedDependencies) { + this.baseRepository_ = baseRepository + } +} -```bash npm2yarn -npm install @myorg/plugin-name +export default HelloModuleService ``` -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. +Then, add the following method that uses it: -First, run the following command to change the version of the plugin: +```ts highlights={repoHighlights} +// ... +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/framework/utils" +import { Context } from "@medusajs/framework/types" +import { EntityManager } from "@mikro-orm/knex" -```bash -npm version -``` +class HelloModuleService { + // ... + @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, + } + ) -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. + // retrieve again + const updatedRecord = await transactionManager.execute( + `SELECT * FROM my_custom WHERE id = '${input.id}'` + ) -Then, re-run the same commands for publishing a plugin: + return updatedRecord + }, + { + transaction: sharedContext.transactionManager, + } + ) + } -```bash -npx medusa plugin:build -npm publish + @InjectManager() + async update( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ) { + return await this.update_(input, sharedContext) + } +} ``` -This will publish an updated version of your plugin under a new version. - - -# Service Factory +The `update_` method uses the `baseRepository_.transaction` method to wrap a function in a transaction. -In this chapter, you’ll learn about what the service factory is and how to use it. +The function parameter receives a transactional entity manager as a parameter. Use it to perform the database operations. -## What is the Service Factory? +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. -Medusa provides a service factory that your module’s main service can extend. +Refer to [MikroORM's reference](https://mikro-orm.io/api/knex/class/EntityManager) for a full list of the entity manager's methods. -The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. +### Transaction Options -Your service provides data-management functionalities of your data models. +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. -## How to Extend the Service Factory? +```ts highlights={[["16"]]} +// other imports... +import { EntityManager } from "@mikro-orm/knex" -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. +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + transaction: sharedContext.transactionManager, + } + ) + } +} +``` -For example, create the file `src/modules/hello/service.ts` with the following content: +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 title="src/modules/hello/service.ts" highlights={highlights} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +```ts highlights={[["19"]]} +// other imports... +import { IsolationLevel } from "@mikro-orm/core" -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - // TODO implement custom methods +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + isolationLevel: IsolationLevel.READ_COMMITTED, + } + ) + } } - -export default HelloModuleService ``` -### MedusaService Parameters +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. -The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. +```ts highlights={[["16"]]} +class HelloModuleService { + // ... + @InjectTransactionManager() + async update_( + input: { + id: string, + name: string + }, + @MedusaContext() sharedContext?: Context + ): Promise { + return await this.baseRepository_.transaction( + async (transactionManager) => { + // ... + }, + { + enableNestedTransactions: false, + } + ) + } +} +``` -In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. -### Generated Methods +# Module Isolation -The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. -The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. +- 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. -For example, the following methods are generated for the service above: +## How are Modules Isolated? -Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) +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. -### listMyCustoms +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. -### listMyCustoms +*** -This method retrieves an array of records based on filters and pagination configurations. +## Why are Modules Isolated -For example: +Some of the module isolation's benefits include: -```ts -const myCustoms = await helloModuleService - .listMyCustoms() +- 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. -// with filters -const myCustoms = await helloModuleService - .listMyCustoms({ - id: ["123"] - }) -``` +*** -### listAndCount +## How to Extend Data Model of Another Module? -### retrieveMyCustom +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). -This method retrieves a record by its ID. +*** -For example: +## How to Use Services of Other Modules? -```ts -const myCustom = await helloModuleService - .retrieveMyCustom("123") -``` +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. -### retrieveMyCustom +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. -### updateMyCustoms +### Example -This method updates and retrieves records of the data model. +For example, consider you have two modules: -For example: +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). -```ts -const myCustom = await helloModuleService - .updateMyCustoms({ - id: "123", - name: "test" - }) +To sync brands from your application to the third-party system, create the following steps: -// update multiple -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - id: "123", - name: "test" - }, - { - id: "321", - name: "test 2" - }, - ]) +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brandModuleService" + ) -// use filters -const myCustoms = await helloModuleService - .updateMyCustoms([ - { - selector: { - id: ["123", "321"] - }, - data: { - name: "test" - } - }, - ]) -``` + const brands = await brandModuleService.listBrands() -### createMyCustoms + return new StepResponse(brands) + } +) -### softDeleteMyCustoms +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) -This method soft-deletes records using an array of IDs or an object of filters. + const cmsBrands = await cmsModuleService.createBrands(brands) -For example: + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cmsModuleService" + ) -```ts -await helloModuleService.softDeleteMyCustoms("123") + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` -// soft-delete multiple -await helloModuleService.softDeleteMyCustoms([ - "123", "321" -]) +The `retrieveBrandsStep` retrieves the brands from a brand module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS module. -// use filters -await helloModuleService.softDeleteMyCustoms({ - id: ["123", "321"] -}) +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) + } +) ``` -### updateMyCustoms +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. -### deleteMyCustoms -### softDeleteMyCustoms +# Modules Directory Structure -### restoreMyCustoms +In this document, you'll learn about the expected files and directories in your module. -### Using a Constructor +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) -If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. +## index.ts -For example: +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). -```ts highlights={[["8"]]} -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" +*** -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - constructor() { - super(...arguments) - } -} +## service.ts -export default HelloModuleService -``` +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). +*** -# Expose a Workflow Hook +## Other Directories -In this chapter, you'll learn how to expose a hook in your workflow. +The following directories are optional and their content are explained more in the following chapters: -## When to Expose a Hook +- `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. -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. +# Loaders -*** +In this chapter, you’ll learn about loaders and how to use them. -## How to Expose a Hook in a Workflow? +## What is a Loader? -To expose a hook in your workflow, use `createHook` from the Workflows SDK. +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. -For example: +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. -```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" +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. -export const myWorkflow = createWorkflow( - "my-workflow", - function (input) { - const product = createProductStep(input) - const productCreatedHook = createHook( - "productCreated", - { productId: product.id } - ) +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). - return new WorkflowResponse(product, { - hooks: [productCreatedHook], - }) - } -) -``` +*** -The `createHook` function accepts two parameters: +## How to Create a Loader? -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. +### 1. Implement Loader Function -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. +You create a loader function in a TypeScript or JavaScript file under a module's `loaders` directory. -### How to Consume the Hook? +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: -To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.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) -```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} -import { myWorkflow } from "../my-workflow" +Learn how to create a module in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -myWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action - } -) -``` +```ts title="src/modules/hello/loaders/hello-world.ts" +import { + LoaderOptions, +} from "@medusajs/framework/types" -The hook is available on the workflow's `hooks` property using its name `productCreated`. +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve("logger") -You invoke the hook, passing a step function (the hook handler) as a parameter. + logger.info("[helloWorldLoader]: Hello, World!") +} +``` +The loader file exports an async function, which is the function executed when the application loads. -# Access Workflow Errors +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. -In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. +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). -## How to Access Workflow Errors? +### 2. Export Loader in Module Definition -By default, when an error occurs in a workflow, it throws that error, and the execution stops. +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. -You can configure the workflow to return the errors instead so that you can access and handle them differently. +So, to export the loader you implemented above in the `hello` module, add the following to `src/modules/hello/index.ts`: -For example: +```ts title="src/modules/hello/index.ts" +// other imports... +import helloWorldLoader from "./loaders/hello-world" -```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +export default Module("hello", { + // ... + loaders: [helloWorldLoader], +}) +``` -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result, errors } = await myWorkflow(req.scope) - .run({ - // ... - throwOnError: false, - }) +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. - if (errors.length) { - return res.send({ - errors: errors.map((error) => error.error), - }) - } +### Test the Loader - res.send(result) -} +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 ``` -The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. - -The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. - +Then, you'll find the following message logged in the terminal: -# Compensation Function +```plain +info: [HELLO MODULE] Just started the Medusa application! +``` -In this chapter, you'll learn what a compensation function is and how to add it to a step. +This indicates that the loader in the `hello` module ran and logged this message. -## What is a Compensation Function +*** -A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. +## Example: Register Custom MongoDB Connection -For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. +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. -By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. +Consider your have a MongoDB module that allows you to perform operations on a MongoDB database. -*** +### Prerequisites -## How to add a Compensation Function? +- [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) -A compensation function is passed as a second parameter to the `createStep` function. +To connect to the database, you create the following loader in your module: -For example, create the file `src/workflows/hello-world.ts` with the following content: +```ts title="src/modules/mongo/loaders/connection.ts" highlights={loaderHighlights} +import { LoaderOptions } from "@medusajs/framework/types" +import { asValue } from "awilix" +import { MongoClient } from "mongodb" -```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +type ModuleOptions = { + connection_url?: string + db_name?: string +} -const step1 = createStep( - "step-1", - async () => { - const message = `Hello from step one!` +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) - console.log(message) + logger.info("Connected to MongoDB") - return new StepResponse(message) - }, - async () => { - console.log("Oops! Rolling back my changes...") + container.register( + "mongoClient", + asValue(clientDb) + ) + } catch (e) { + logger.error( + `[MONGO MDOULE]: An error occurred while connecting to MongoDB: ${e}` + ) } -) +} ``` -Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. - -*** - -## Test the Compensation Function - -Create a step in the same `src/workflows/hello-world.ts` file that throws an error: +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="src/workflows/hello-world.ts" -const step2 = createStep( - "step-2", - async () => { - throw new Error("Throwing an error...") - } -) +```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, + }, + }, + ], +}) ``` -Then, create a workflow that uses the steps: +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: -```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -// other imports... +- `connection_url`: the URL to connect to the MongoDB database. +- `db_name`: The name of the database to connect to. -// steps... +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. -const myWorkflow = createWorkflow( - "hello-world", - function (input) { - const str1 = step1() - step2() +After creating the client, you register it in the module's container using the container's `register` method. The method accepts two parameters: - return new WorkflowResponse({ - message: str1, - }) -}) +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. -export default myWorkflow -``` +### Use Custom Registered Resource in Module's Service -Finally, execute the workflow from an API route: +After registering the custom MongoDB client in the module's container, you can now resolve and use it in the module's service. -```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +For example: -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await myWorkflow(req.scope) - .run() +```ts title="src/modules/mongo/service.ts" +import type { Db } from "mongodb" - res.send(result) +type InjectedDependencies = { + mongoClient: Db } -``` -Run the Medusa application and send a `GET` request to `/workflow`: +export default class MongoModuleService { + private mongoClient_: Db -```bash -curl http://localhost:9000/workflow -``` + constructor({ mongoClient }: InjectedDependencies) { + this.mongoClient_ = mongoClient + } -In the console, you'll see: + async createMovie({ title }: { + title: string + }) { + const moviesCol = this.mongoClient_.collection("movie") -- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. -- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. + const insertedMovie = await moviesCol.insertOne({ + title, + }) -*** + const movie = await moviesCol.findOne({ + _id: insertedMovie.insertedId, + }) -## Pass Input to Compensation Function + return movie + } -If a step creates a record, the compensation function must receive the ID of the record to remove it. + async deleteMovie(id: string) { + const moviesCol = this.mongoClient_.collection("movie") -To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. + await moviesCol.deleteOne({ + _id: { + equals: id, + }, + }) + } +} +``` -For example: +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. -```ts highlights={inputHighlights} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +Make sure to export the loader in the module's definition in the `index.ts` file at the root directory of the module: -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }) => { - console.log(message) - } -) -``` +```ts title="src/modules/mongo/index.ts" highlights={[["9"]]} +import { Module } from "@medusajs/framework/utils" +import MongoModuleService from "./service" +import mongoConnectionLoader from "./loaders/connection" -In this example, the step passes an object as a second parameter to `StepResponse`. +export const MONGO_MODULE = "mongo" -The compensation function receives the object and uses its `message` property to log a message. +export default Module(MONGO_MODULE, { + service: MongoModuleService, + loaders: [mongoConnectionLoader], +}) +``` -*** +### Test it Out -## Resolve Resources from the Medusa Container +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: -The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. +```bash +info: Connected to MongoDB +``` -For example: +You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -```ts -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -const step1 = createStep( - "step-1", - async () => { - return new StepResponse( - `Hello from step one!`, - { message: "Oops! Rolling back my changes..." } - ) - }, - async ({ message }, { container }) => { - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) +# Multiple Services in a Module - logger.info(message) - } -) -``` +In this chapter, you'll learn how to use multiple services in a module. -In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. +## Module's Main and Internal Services -You then use the logger to log a message. +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. *** -## Handle Errors in Loops +## How to Add an Internal Service -This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). +### 1. Create Service -Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: +To add an internal service, create it in the `services` directory of your module. -```ts -// other imports... -import { promiseAll } from "@medusajs/framework/utils" +For example, create the file `src/modules/hello/services/client.ts` with the following content: -type StepInput = { - ids: string[] +```ts title="src/modules/hello/services/client.ts" +export class ClientService { + async getMessage(): Promise { + return "Hello, World!" + } } +``` -const step1 = createStep( - "step-1", - async ({ ids }: StepInput, { container }) => { - const erpModuleService = container.resolve( - ERP_MODULE - ) - const prevData: unknown[] = [] - - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +### 2. Export Service in Index - await erpModuleService.delete(id) +Next, create an `index.ts` file under the `services` directory of the module that exports your internal services. - prevData.push(id) - }) - ) +For example, create the file `src/modules/hello/services/index.ts` with the following content: - return new StepResponse(ids, prevData) - } -) +```ts title="src/modules/hello/services/index.ts" +export * from "./client" ``` -In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. +This exports the `ClientService`. -However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. +### 3. Resolve Internal Service -To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: +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. -```ts highlights={highlights} -try { - await promiseAll( - ids.map(async (id) => { - const data = await erpModuleService.retrieve(id) +For example, in your main service: - await erpModuleService.delete(id) +```ts title="src/modules/hello/service.ts" highlights={[["5"], ["13"]]} +// other imports... +import { ClientService } from "./services" - prevData.push(id) - }) - ) -} catch (e) { - return StepResponse.permanentFailure( - `An error occurred: ${e}`, - prevData - ) +type InjectedDependencies = { + clientService: ClientService } -``` -The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected clientService_: ClientService -So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. + constructor({ clientService }: InjectedDependencies) { + super(...arguments) + this.clientService_ = clientService + } +} +``` +You can now use your internal service in your main service. -# 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. +## Resolve Resources in Internal Service -## Why If-Conditions Aren't Allowed in Workflows? +Resolve dependencies from your module's container in the constructor of your internal service. -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. +For example: -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. +```ts +import { Logger } from "@medusajs/framework/types" -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. +type InjectedDependencies = { + logger: Logger +} -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. +export class ClientService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + } +} +``` *** -## How to use When-Then? +## Access Module Options -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. +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 highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... +```ts +import { ConfigModule } from "@medusajs/framework/types" +import { HELLO_MODULE } from ".." -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { +export type InjectedDependencies = { + configModule: ConfigModule +} - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) +export class ClientService { + protected options: Record - // executed without condition - const anotherStepResult = anotherStep(result) + constructor({ configModule }: InjectedDependencies) { + const moduleDef = configModule.modules[HELLO_MODULE] - return new WorkflowResponse( - anotherStepResult - ) + if (typeof moduleDef !== "boolean") { + this.options = moduleDef.options + } } -) +} ``` -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters +The `configModule` has a `modules` property that includes all registered modules. Retrieve the module's configuration using its registration key. -`when` accepts the following parameters: +If its value is not a `boolean`, set the service's options to the module configuration's `options` property. -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 +# Service Constraints -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. +This chapter lists constraints to keep in mind when creating a service. -The callback function is only executed if `when`'s second parameter function returns a `true` value. +## 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. -## Implementing If-Else with When-Then +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: -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. +```ts +await helloModuleService.getMessage() +``` -For example: +So, make sure your service's methods are always async to avoid unexpected errors or behavior. -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { +```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } - // ... + // Do + async getMessage(): Promise { + return "Hello, World!" } -) +} + +export default HelloModuleService ``` -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. -*** +# Service Factory -## Specify Name for When-Then +In this chapter, you’ll learn about what the service factory is and how to use it. -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: +## What is the Service Factory? -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` +Medusa provides a service factory that your module’s main service can extend. -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. +The service factory generates data management methods for your data models in the database, so you don't have to implement these methods manually. -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. +Your service provides data-management functionalities of your data models. -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() +## How to Extend the Service Factory? - return { - isActive, - } -}) -``` +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. -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: +For example, create the file `src/modules/hello/service.ts` with the following content: -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`. +```ts title="src/modules/hello/service.ts" highlights={highlights} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -The second and third parameters are the same as the parameters you previously passed to `when`. +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + // TODO implement custom methods +} +export default HelloModuleService +``` -# Workflow Constraints +### MedusaService Parameters -This chapter lists constraints of defining a workflow or its steps. +The `MedusaService` function accepts one parameter, which is an object of data models to generate data-management methods for. -## Workflow Constraints +In the example above, since the `HelloModuleService` extends `MedusaService`, it has methods to manage the `MyCustom` data model, such as `createMyCustoms`. -### No Async Functions +### Generated Methods -The function passed to `createWorkflow` can’t be an async function: +The service factory generates methods to manage the records of each of the data models provided in the first parameter in the database. -```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - async function (input: WorkflowInput) { - // ... -}) +The method's names are the operation's name, suffixed by the data model's key in the object parameter passed to `MedusaService`. -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - // ... -}) -``` +For example, the following methods are generated for the service above: -### No Direct Variable Manipulation +Find a complete reference of each of the methods in [this documentation](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) -You can’t directly manipulate variables within the workflow's constructor function. +### listMyCustoms -Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) +### listMyCustoms -Instead, use `transform` from the Workflows SDK: +This method retrieves an array of records based on filters and pagination configurations. -```ts highlights={highlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) +For example: - return new WorkflowResponse({ - message: `${str1}${str2}`, +```ts +const myCustoms = await helloModuleService + .listMyCustoms() + +// with filters +const myCustoms = await helloModuleService + .listMyCustoms({ + id: ["123"] }) -}) +``` -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) +### listAndCount - const result = transform( - { - str1, - str2, - }, - (input) => ({ - message: `${input.str1}${input.str2}`, - }) - ) +### retrieveMyCustom - return new WorkflowResponse(result) -}) -``` +This method retrieves a record by its ID. -### Create Dates in transform +For example: -When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution. +```ts +const myCustom = await helloModuleService + .retrieveMyCustom("123") +``` -Instead, create the date using `transform`. +### retrieveMyCustom -Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +### updateMyCustoms -For example: +This method updates and retrieves records of the data model. -```ts highlights={dateHighlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = new Date() +For example: - return new WorkflowResponse({ - today, +```ts +const myCustom = await helloModuleService + .updateMyCustoms({ + id: "123", + name: "test" }) -}) -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = transform({}, () => new Date()) +// update multiple +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + id: "123", + name: "test" + }, + { + id: "321", + name: "test 2" + }, + ]) - return new WorkflowResponse({ - today, - }) -}) +// use filters +const myCustoms = await helloModuleService + .updateMyCustoms([ + { + selector: { + id: ["123", "321"] + }, + data: { + name: "test" + } + }, + ]) ``` -### No If Conditions +### createMyCustoms -You can't use if-conditions in a workflow. +### softDeleteMyCustoms -Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) +This method soft-deletes records using an array of IDs or an object of filters. -Instead, use when-then from the Workflows SDK: +For example: ```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - if (input.is_active) { - // perform an action - } -}) +await helloModuleService.softDeleteMyCustoms("123") -// Do (explained in the next chapter) -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - when(input, (input) => { - return input.is_active - }) - .then(() => { - // perform an action - }) -}) -``` +// soft-delete multiple +await helloModuleService.softDeleteMyCustoms([ + "123", "321" +]) -You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). +// use filters +await helloModuleService.softDeleteMyCustoms({ + id: ["123", "321"] +}) +``` -### No Conditional Operators +### updateMyCustoms -You can't use conditional operators in a workflow, such as `??` or `||`. +### deleteMyCustoms -Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) +### softDeleteMyCustoms -Instead, use `transform` to store the desired value in a variable. +### restoreMyCustoms -### Logical Or (||) Alternative +### Using a Constructor -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message || "Hello" -}) +If you implement the `constructor` of your service, make sure to call `super` passing it `...arguments`. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +For example: -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message || "hello" - ) -}) -``` +```ts highlights={[["8"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -### Nullish Coalescing (??) Alternative +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + constructor() { + super(...arguments) + } +} -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message ?? "Hello" -}) +export default HelloModuleService +``` -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => data.input.message ?? "hello" - ) -}) -``` +# Module Options -### Double Not (!!) Alternative +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. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - isActive: !!input.is_active, - }) -}) +## What are Module Options? -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +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. -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const isActive = transform( - { - input, - }, - (data) => !!data.input.is_active - ) - - step1({ - isActive, - }) -}) -``` +*** -### Ternary Alternative +## How to Pass Options to a Module? -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - message: input.is_active ? "active" : "inactive", - }) -}) +To pass options to a module, add an `options` property to the module’s configuration in `medusa-config.ts`. -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +For example: -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/hello", + options: { + capitalize: true, }, - (data) => { - return data.input.is_active ? "active" : "inactive" - } - ) - - step1({ - message, - }) + }, + ], }) ``` -### Optional Chaining (?.) Alternative +The `options` property’s value is an object. You can pass any properties you want. -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - name: input.customer?.name, - }) -}) +### Pass Options to a Module in a Plugin -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" +If your module is part of a plugin, you can pass options to the module in the plugin’s configuration. -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const name = transform( - { - input, +For example: + +```ts title="medusa-config.ts" +import { defineConfig } from "@medusajs/framework/utils" +module.exports = defineConfig({ + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + capitalize: true, }, - (data) => data.input.customer?.name - ) - - step1({ - name, - }) + }, + ], }) ``` +The `options` property in the plugin configuration is passed to all modules in a plugin. + *** -## Step Constraints +## Access Module Options in Main Service -### Returned Values +The module’s main service receives the module options as a second parameter. -A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. +For example: -Values of other types, such as Maps, aren't allowed. +```ts title="src/modules/hello/service.ts" highlights={[["12"], ["14", "options?: ModuleOptions"], ["17"], ["18"], ["19"]]} +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -```ts -// Don't -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean +} -const step1 = createStep( - "step-1", - (input, { container }) => { - const myMap = new Map() +export default class HelloModuleService extends MedusaService({ + MyCustom, +}){ + protected options_: ModuleOptions - // ... + constructor({}, options?: ModuleOptions) { + super(...arguments) - return new StepResponse({ - myMap, - }) + this.options_ = options || { + capitalize: false, + } } -) - -// Do -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -const step1 = createStep( - "step-1", - (input, { container }) => { - const myObj: Record = {} - - // ... - - return new StepResponse({ - myObj, - }) - } -) + // ... +} ``` +*** -# Run Workflow Steps in Parallel - -In this chapter, you’ll learn how to run workflow steps in parallel. +## Access Module Options in Loader -## parallelize Utility Function +The object that a module’s loaders receive as a parameter has an `options` property holding the module's options. -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. +For example: -The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. - -For example: - -```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - parallelize, -} from "@medusajs/framework/workflows-sdk" +```ts title="src/modules/hello/loaders/hello-world.ts" highlights={[["11"], ["12", "ModuleOptions", "The type of expected module options."], ["16"]]} import { - createProductStep, - getProductStep, - createPricesStep, - attachProductToSalesChannelStep, -} from "./steps" + LoaderOptions, +} from "@medusajs/framework/types" -interface WorkflowInput { - title: string +// recommended to define type in another file +type ModuleOptions = { + capitalize?: boolean } -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) - - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) - - const id = product.id - const refetchedProduct = getProductStep(product.id) - - return new WorkflowResponse(refetchedProduct) - } -) +export default async function helloWorldLoader({ + options, +}: LoaderOptions) { + + console.log( + "[HELLO MODULE] Just started the Medusa application!", + options + ) +} ``` -The `parallelize` function accepts the steps to run in parallel as a parameter. - -It returns an array of the steps' results in the same order they're passed to the `parallelize` function. - -So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. - - -# Retry Failed Steps -In this chapter, you’ll learn how to configure steps to allow retrial on failure. +# Scheduled Jobs Number of Executions -## Configure a Step’s Retrial +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. -By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. +## numberOfExecutions Option -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. +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 title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - }, - async () => { - console.log("Executing step 1") +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` - throw new Error("Oops! Something happened.") - } -) +The above scheduled job has the `numberOfExecutions` configuration set to `3`. -const myWorkflow = createWorkflow( - "hello-world", - function () { - const str1 = step1() +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - return new WorkflowResponse({ - message: str1, - }) -}) +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. -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. +# Access Workflow Errors -When you execute the above workflow, you’ll see the following result in the terminal: +In this chapter, you’ll learn how to access errors that occur during a workflow’s execution. -```bash -Executing step 1 -Executing step 1 -Executing step 1 -error: Oops! Something happened. -Error: Oops! Something happened. -``` +## How to Access Workflow Errors? -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. +By default, when an error occurs in a workflow, it throws that error, and the execution stops. -*** +You can configure the workflow to return the errors instead so that you can access and handle them differently. -## Step Retry Intervals +For example: -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. +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" -For example: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result, errors } = await myWorkflow(req.scope) + .run({ + // ... + throwOnError: false, + }) -```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} -const step1 = createStep( - { - name: "step-1", - maxRetries: 2, - retryInterval: 2, // 2 seconds - }, - async () => { - // ... + if (errors.length) { + return res.send({ + errors: errors.map((error) => error.error), + }) } -) + + res.send(result) +} + ``` -### Interval Changes Workflow to Long-Running +The object passed to the `run` method accepts a `throwOnError` property. When disabled, the errors are returned in the `errors` property of `run`'s output. -By setting `retryInterval` on a step, a workflow becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. So, you won't receive its result or errors immediately when you execute the workflow. +The value of `errors` is an array of error objects. Each object has an `error` property, whose value is the name or text of the thrown error. -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). +# Expose a Workflow Hook -# Multiple Step Usage in Workflow +In this chapter, you'll learn how to expose a hook in your workflow. -In this chapter, you'll learn how to use a step multiple times in a workflow. +## When to Expose a Hook -## Problem Reusing a Step in a Workflow +Your workflow is reusable in other applications, and you allow performing an external action at some point in your workflow. -In some cases, you may need to use a step multiple times in the same workflow. +Your workflow isn't reusable by other applications. Use a step that performs what a hook handler would instead. -The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. +*** -Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: +## How to Expose a Hook in a Workflow? -```ts -const useQueryGraphStep = createStep( - "use-query-graph" - // ... -) -``` +To expose a hook in your workflow, use `createHook` from the Workflows SDK. -This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: +For example: -```ts -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +```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" - // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], +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 next section explains how to fix this issue to use the same step multiple times in a workflow. - -*** +The `createHook` function accepts two parameters: -## How to Use a Step Multiple Times in a Workflow? +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. -When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. +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. -Use the `config` method to change a step's ID for a single execution. +### How to Consume the Hook? -So, this is the correct way to write the example above: +To consume the hook of the workflow, create the file `src/workflows/hooks/my-workflow.ts` with the following content: -```ts highlights={highlights} -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) +```ts title="src/workflows/hooks/my-workflow.ts" highlights={handlerHighlights} +import { myWorkflow } from "../my-workflow" - // ✓ No error occurs, the step has a different ID. - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }).config({ name: "fetch-customers" }) +myWorkflow.hooks.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action } ) ``` -The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. +The hook is available on the workflow's `hooks` property using its name `productCreated`. -The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. +You invoke the hook, passing a step function (the hook handler) as a parameter. -# Execute Another Workflow +# Compensation Function -In this chapter, you'll learn how to execute a workflow in another. +In this chapter, you'll learn what a compensation function is and how to add it to a step. -## Execute in a Workflow +## What is a Compensation Function -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: [ - // ... - ], - }, - }) - - // ... - } -) -``` +A compensation function rolls back or undoes changes made by a step when an error occurs in the workflow. -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. +For example, if a step creates a record, the compensation function deletes the record when an error occurs later in the workflow. -The object has an `input` property to pass input to the workflow. +By using compensation functions, you provide a mechanism that guarantees data consistency in your application and across systems. *** -## 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. +## How to add a Compensation Function? -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). +A compensation function is passed as a second parameter to the `createStep` function. -For example: +For example, create the file `src/workflows/hello-world.ts` with the following content: -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" +```ts title="src/workflows/hello-world.ts" highlights={[["15"], ["16"], ["17"]]} collapsibleLines="1-5" expandButtonLabel="Show Imports" import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -type WorkflowInput = { - title: string -} + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) +const step1 = createStep( + "step-1", + async () => { + const message = `Hello from step one!` - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) + console.log(message) - // ... + return new StepResponse(message) + }, + async () => { + console.log("Oops! Rolling back my changes...") } ) ``` -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`. +Each step can have a compensation function. The compensation function only runs if an error occurs throughout the workflow. *** -## Run Workflow Conditionally +## Test the Compensation Function -To run a workflow in another based on a condition, use when-then from the Workflows SDK. +Create a step in the same `src/workflows/hello-world.ts` file that throws an error: -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). +```ts title="src/workflows/hello-world.ts" +const step2 = createStep( + "step-2", + async () => { + throw new Error("Throwing an error...") + } +) +``` -For example: +Then, create a workflow that uses the steps: -```ts highlights={whenHighlights} collapsibleLines="1-16" +```ts title="src/workflows/hello-world.ts" collapsibleLines="1-8" expandButtonLabel="Show Imports" import { createWorkflow, - when, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" -import { - CreateProductWorkflowInputDTO, -} from "@medusajs/framework/types" +// other imports... -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} +// steps... -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) -``` +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1() + step2() -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + return new WorkflowResponse({ + message: str1, + }) +}) +export default myWorkflow +``` -# Long-Running Workflows +Finally, execute the workflow from an API route: -In this chapter, you’ll learn what a long-running workflow is and how to configure it. +```ts title="src/api/workflow/route.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" -## What is a Long-Running Workflow? +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await myWorkflow(req.scope) + .run() -When you execute a workflow, you wait until the workflow finishes execution to receive the output. + res.send(result) +} +``` -A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. +Run the Medusa application and send a `GET` request to `/workflow`: -### Why use Long-Running Workflows? +```bash +curl http://localhost:9000/workflow +``` -Long-running workflows are useful if: +In the console, you'll see: -- A task takes too long. For example, you're importing data from a CSV file. -- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user. +- `Hello from step one!` logged in the terminal, indicating that the first step ran successfully. +- `Oops! Rolling back my changes...` logged in the terminal, indicating that the second step failed and the compensation function of the first step ran consequently. *** -## Configure Long-Running Workflows +## Pass Input to Compensation Function -A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response. +If a step creates a record, the compensation function must receive the ID of the record to remove it. -For example, consider the following workflow and steps: +To pass input to the compensation function, pass a second parameter in the `StepResponse` returned by the step. -```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More" +For example: + +```ts highlights={inputHighlights} import { - createStep, - createWorkflow, - WorkflowResponse, + createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -const step1 = createStep("step-1", async () => { - return new StepResponse({}) -}) - -const step2 = createStep( - { - name: "step-2", - async: true, - }, +const step1 = createStep( + "step-1", async () => { - console.log("Waiting to be successful...") + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }) => { + console.log(message) } ) +``` -const step3 = createStep("step-3", async () => { - return new StepResponse("Finished three steps") -}) +In this example, the step passes an object as a second parameter to `StepResponse`. -const myWorkflow = createWorkflow( - "hello-world", - function () { - step1() - step2() - const message = step3() +The compensation function receives the object and uses its `message` property to log a message. - return new WorkflowResponse({ - message, - }) -}) +*** -export default myWorkflow -``` +## Resolve Resources from the Medusa Container -The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step. +The compensation function receives an object second parameter. The object has a `container` property that you use to resolve resources from the Medusa container. -So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. +For example: -A workflow is also considered long-running if one of its steps has their `retryInterval` option set as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). +```ts +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -*** +const step1 = createStep( + "step-1", + async () => { + return new StepResponse( + `Hello from step one!`, + { message: "Oops! Rolling back my changes..." } + ) + }, + async ({ message }, { container }) => { + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) -## Change Step Status + logger.info(message) + } +) +``` -Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step. +In this example, you use the `container` property in the second object parameter of the compensation function to resolve the logger. -To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. +You then use the logger to log a message. -### Retrieve Transaction ID - -Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. +*** -When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses: +## Handle Errors in Loops -```ts -const { transaction } = await myWorkflow(req.scope) - .run() +This feature is only available after [Medusa v2.0.5](https://github.com/medusajs/medusa/releases/tag/v2.0.5). -// use transaction.transactionId later -``` +Consider you have a module that integrates a third-party ERP system, and you're creating a workflow that deletes items in that ERP. You may have the following step: -### Change Step Status to Successful +```ts +// other imports... +import { promiseAll } from "@medusajs/framework/utils" -The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. +type StepInput = { + ids: string[] +} -For example, consider the following step: +const step1 = createStep( + "step-1", + async ({ ids }: StepInput, { container }) => { + const erpModuleService = container.resolve( + ERP_MODULE + ) + const prevData: unknown[] = [] -```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) -type SetStepSuccessStepInput = { - transactionId: string -}; + await erpModuleService.delete(id) -export const setStepSuccessStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepSuccessStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE + prevData.push(id) + }) ) - await workflowEngineService.setStepSuccess({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Done!"), - options: { - container, - }, - }) + return new StepResponse(ids, prevData) } ) ``` -In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful. - -The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: +In the step, you loop over the IDs to retrieve the item's data, store them in a `prevData` variable, then delete them using the ERP Module's service. You then pass the `prevData` variable to the compensation function. -- idempotencyKey: (\`object\`) The details of the workflow execution. +However, if an error occurs in the loop, the `prevData` variable won't be passed to the compensation function as the execution never reached the return statement. - - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. +To handle errors in the loop so that the compensation function receives the last version of `prevData` before the error occurred, you wrap the loop in a try-catch block. Then, in the catch block, you invoke and return the `StepResponse.permanentFailure` function: - - transactionId: (\`string\`) The ID of the workflow execution's transaction. +```ts highlights={highlights} +try { + await promiseAll( + ids.map(async (id) => { + const data = await erpModuleService.retrieve(id) - - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. + await erpModuleService.delete(id) - - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. -- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status. -- options: (\`Record\\`) Options to pass to the step. + prevData.push(id) + }) + ) +} catch (e) { + return StepResponse.permanentFailure( + `An error occurred: ${e}`, + prevData + ) +} +``` - - container: (\`MedusaContainer\`) An instance of the Medusa Container +The `StepResponse.permanentFailure` fails the step and its workflow, triggering current and previous steps' compensation functions. The `permanentFailure` function accepts as a first parameter the error message, which is saved in the workflow's error details, and as a second parameter the data to pass to the compensation function. -### Change Step Status to Failed +So, if an error occurs during the loop, the compensation function will still receive the `prevData` variable to undo the changes made before the step failed. -The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. -After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. +# Conditions in Workflows with When-Then -For example: +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. -```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - Modules, - TransactionHandlerType, -} from "@medusajs/framework/utils" -import { - StepResponse, - createStep, -} from "@medusajs/framework/workflows-sdk" +## Why If-Conditions Aren't Allowed in Workflows? -type SetStepFailureStepInput = { - transactionId: string -}; +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. -export const setStepFailureStep = createStep( - "set-step-success-step", - async function ( - { transactionId }: SetStepFailureStepInput, - { container } - ) { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) +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. - await workflowEngineService.setStepFailure({ - idempotencyKey: { - action: TransactionHandlerType.INVOKE, - transactionId, - stepId: "step-2", - workflowId: "hello-world", - }, - stepResponse: new StepResponse("Failed!"), - options: { - container, - }, - }) - } -) -``` +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. -You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. *** -## Access Long-Running Workflow Status and Result - -To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service. +## How to use When-Then? -To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md). +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 title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" -import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import myWorkflow from "../../../workflows/hello-world" +```ts highlights={highlights} import { - IWorkflowEngineService, -} from "@medusajs/framework/types" -import { Modules } from "@medusajs/framework/utils" - -export async function GET(req: MedusaRequest, res: MedusaResponse) { - const { transaction, result } = await myWorkflow(req.scope).run() - - const workflowEngineService = req.scope.resolve< - IWorkflowEngineService - >( - Modules.WORKFLOW_ENGINE - ) + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... - const subscriptionOptions = { - workflowId: "hello-world", - transactionId: transaction.transactionId, - subscriberId: "hello-world-subscriber", - } +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { - await workflowEngineService.subscribe({ - ...subscriptionOptions, - subscriber: async (data) => { - if (data.eventType === "onFinish") { - console.log("Finished execution", data.result) - // unsubscribe - await workflowEngineService.unsubscribe({ - ...subscriptionOptions, - subscriberOrId: subscriptionOptions.subscriberId, - }) - } else if (data.eventType === "onStepFailure") { - console.log("Workflow failed", data.step) + const result = when( + input, + (input) => { + return input.is_active } - }, - }) + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) - res.send(result) -} -``` + // executed without condition + const anotherStepResult = anotherStep(result) -In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` -### subscribe Method +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. -The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties: +### When Parameters -- workflowId: (\`string\`) The name of the workflow. -- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. -- subscriberId: (\`string\`) The ID of the subscriber. -- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution. +`when` accepts the following parameters: -If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output. +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`. -### unsubscribe Method +### Then Parameters -You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. -However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. +The callback function is only executed if `when`'s second parameter function returns a `true` value. *** -## Example: Restaurant-Delivery Recipe - -To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). - -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. - +## Implementing If-Else with When-Then -# Store Workflow Executions +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. -In this chapter, you'll learn how to store workflow executions in the database and access them later. +For example: -## Workflow Execution Retention +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { -Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database. + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) -This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed. + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. + // ... + } +) +``` -*** +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. -## How to Store Workflow's Executions? +*** -### Prerequisites +## Specify Name for When-Then -- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) +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: -`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions: +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` -- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default. -- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database. +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. -For example: +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. -```ts highlights={highlights} -import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: -const step1 = createStep( - { - name: "step-1" - }, - async () => { - console.log("Hello from step 1") +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active } -) +).then(() => { + const isActive = isActiveStep() -export const helloWorkflow = createWorkflow( - { - name: "hello-workflow", - retentionTime: 99999, - store: true - }, - () => { - step1() + return { + isActive, } -) +}) ``` -Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. - -*** +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: -## Retrieve Workflow Executions +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`. -You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. +The second and third parameters are the same as the parameters you previously passed to `when`. -When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: -```ts -const { transaction } = await helloWorkflow(container).run() -``` +# Workflow Constraints -To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. +This chapter lists constraints of defining a workflow or its steps. -For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID: +## Workflow Constraints -```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; -import { Modules } from "@medusajs/framework/utils"; +### No Async Functions -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { transaction_id } = req.params - - const workflowEngineService = req.scope.resolve( - Modules.WORKFLOW_ENGINE - ) +The function passed to `createWorkflow` can’t be an async function: - const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ - transaction_id: transaction_id - }) +```ts highlights={[["4", "async", "Function can't be async."], ["11", "", "Correct way of defining the function."]]} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + async function (input: WorkflowInput) { + // ... +}) - res.json({ - workflowExecution - }) -} +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + // ... +}) ``` -In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details. +### No Direct Variable Manipulation -A workflow execution object will be similar to the following: +You can’t directly manipulate variables within the workflow's constructor function. -```json -{ - "workflow_id": "hello-workflow", - "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP", - "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78", - "execution": { - "state": "done", - "steps": {}, - "modelId": "hello-workflow", - "options": {}, - "metadata": {}, - "startedAt": 1737719880027, - "definition": {}, - "timedOutAt": null, - "hasAsyncSteps": false, - "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP", - "hasFailedSteps": false, - "hasSkippedSteps": false, - "hasWaitingSteps": false, - "hasRevertedSteps": false, - "hasSkippedOnFailureSteps": false - }, - "context": { - "data": {}, - "errors": [] - }, - "state": "done", - "created_at": "2025-01-24T09:58:00.036Z", - "updated_at": "2025-01-24T09:58:00.046Z", - "deleted_at": null -} -``` +Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) -### Example: Check if Stored Workflow Execution Failed +Instead, use `transform` from the Workflows SDK: -To check if a stored workflow execution failed, you can check its `state` property: +```ts highlights={highlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) -```ts -if (workflowExecution.state === "failed") { - return res.status(500).json({ - error: "Workflow failed" + return new WorkflowResponse({ + message: `${str1}${str2}`, }) -} -``` +}) -Other state values include `done`, `invoking`, and `compensating`. +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) + const result = transform( + { + str1, + str2, + }, + (input) => ({ + message: `${input.str1}${input.str2}`, + }) + ) -# Variable Manipulation in Workflows with transform + return new WorkflowResponse(result) +}) +``` -In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. +### Create Dates in transform -## Why Variable Manipulation isn't Allowed in Workflows +When you use `new Date()` in a workflow's constructor function, the date is evaluated when Medusa creates the internal representation of the workflow, not during execution. -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. +Instead, create the date using `transform`. -At that point, variables in the workflow don't have any values. They only do when you execute the workflow. +Learn more about how Medusa creates an internal representation of a workflow [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items. +For example: -Instead, use `transform` from the Workflows SDK. +```ts highlights={dateHighlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = new Date() -Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. + return new WorkflowResponse({ + today, + }) +}) -*** +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = transform({}, () => new Date()) -## What is the transform Utility? + return new WorkflowResponse({ + today, + }) +}) +``` -`transform` creates a new variable as the result of manipulating other variables. +### No If Conditions -For example, consider you have two strings as the output of two steps: +You can't use if-conditions in a workflow. -```ts -const str1 = step1() -const str2 = step2() -``` +Learn more about why you can't use if-conditions [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) -To concatenate the strings, you create a new variable `str3` using the `transform` function: - -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +Instead, use when-then from the Workflows SDK: +```ts +// Don't const myWorkflow = createWorkflow( "hello-world", - function (input) { - const str1 = step1(input) - const str2 = step2(input) - - const str3 = transform( - { str1, str2 }, - (data) => `${data.str1}${data.str2}` - ) - - return new WorkflowResponse(str3) + function (input: WorkflowInput) { + if (input.is_active) { + // perform an action } -) -``` - -`transform` accepts two parameters: +}) -1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function. -2. The second parameter is the function performing the variable manipulation. +// Do (explained in the next chapter) +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + when(input, (input) => { + return input.is_active + }) + .then(() => { + // perform an action + }) +}) +``` -The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. +You can also pair multiple `when-then` blocks to implement an `if-else` condition as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). -You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response. +### No Conditional Operators -*** +You can't use conditional operators in a workflow, such as `??` or `||`. -## Example: Looping Over Array +Learn more about why you can't use conditional operators [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows/index.html.md) -Use `transform` to loop over arrays to create another variable from the array's items. +Instead, use `transform` to store the desired value in a variable. -For example: +### Logical Or (||) Alternative -```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -// step imports... +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message || "Hello" +}) -type WorkflowInput = { - items: { - id: string - name: string - }[] -} +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" const myWorkflow = createWorkflow( "hello-world", - function ({ items }: WorkflowInput) { - const ids = transform( - { items }, - (data) => data.items.map((item) => item.id) + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => data.input.message || "hello" ) - - doSomethingStep(ids) - - // ... - } -) +}) ``` -This workflow receives an `items` array in its input. - -You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. - -You then pass the `ids` variable as a parameter to the `doSomethingStep`. - -*** - -## Example: Creating a Date - -If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed. - -So, use `transform` instead to create a date variable with `new Date()`. - -For example: +### Nullish Coalescing (??) Alternative ```ts +// Don't const myWorkflow = createWorkflow( - "hello-world", - () => { - const today = transform({}, () => new Date()) - - doSomethingStep(today) - } -) -``` - -In this workflow, `today` is only evaluated when the workflow is executed. + "hello-world", + function (input: WorkflowInput) { + const message = input.message ?? "Hello" +}) -*** +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -## Caveats +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => data.input.message ?? "hello" + ) +}) +``` -### Transform Evaluation +### Double Not (!!) Alternative -`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + isActive: !!input.is_active, + }) +}) -For example, if you have the following workflow: +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -```ts const myWorkflow = createWorkflow( "hello-world", - function (input) { - const str = transform( - { input }, - (data) => `${data.input.str1}${data.input.str2}` + function (input: WorkflowInput) { + const isActive = transform( + { + input, + }, + (data) => !!data.input.is_active ) - - return new WorkflowResponse("done") - } -) + + step1({ + isActive, + }) +}) ``` -Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. - -### Data Validation - -`transform` should only be used to perform variable or data manipulation. +### Ternary Alternative -If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead. +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + message: input.is_active ? "active" : "inactive", + }) +}) -For example: +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" -```ts -// DON'T const myWorkflow = createWorkflow( "hello-world", - function (input) { - const str = transform( - { input }, + function (input: WorkflowInput) { + const message = transform( + { + input, + }, (data) => { - if (!input.str1) { - throw new Error("Not allowed!") - } + return data.input.is_active ? "active" : "inactive" } ) - } -) + + step1({ + message, + }) +}) +``` -// DO -const validateHasStr1Step = createStep( - "validate-has-str1", - ({ input }) => { - if (!input.str1) { - throw new Error("Not allowed!") - } - } -) +### Optional Chaining (?.) Alternative +```ts +// Don't const myWorkflow = createWorkflow( "hello-world", - function (input) { - validateHasStr1({ - input, + function (input: WorkflowInput) { + step1({ + name: input.customer?.name, }) +}) - // workflow continues its execution only if - // the step doesn't throw the error. - } -) +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" + +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const name = transform( + { + input, + }, + (data) => data.input.customer?.name + ) + + step1({ + name, + }) +}) ``` +*** -# 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. +## Step Constraints -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. +### Returned Values -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. +A step must only return serializable values, such as [primitive values](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values) or an object. -You want to perform a custom action during a workflow's execution, such as when a product is created. +Values of other types, such as Maps, aren't allowed. -*** +```ts +// Don't +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -## How to Consume a Hook? +const step1 = createStep( + "step-1", + (input, { container }) => { + const myMap = new Map() -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: + return new StepResponse({ + myMap, + }) + } +) -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. +// Do +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: +const step1 = createStep( + "step-1", + (input, { container }) => { + const myObj: Record = {} -```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 + return new StepResponse({ + myObj, + }) } ) ``` -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/index.html.md), 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. +# Execute Another Workflow -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. +In this chapter, you'll learn how to execute a workflow in another. -### Hook Handler Compensation +## Execute in a Workflow -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. +To execute a workflow in another, use the `runAsStep` method that every workflow has. For example: -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -createProductsWorkflow.hooks.productCreated( - async ({ productId }, { container }) => { - // TODO perform an action +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) - 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. +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. -### Additional Data Property +The object has an `input` property to pass input to the workflow. -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" +## Preparing Input Data -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action - } -) -``` +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. -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 about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). -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). +For example: -### Pass Additional Data to Workflow +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: +type WorkflowInput = { + title: string +} -```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" +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, }, - }, - }) -} -``` + }) -Your hook handler then receives that passed data in the `additional_data` object. + // ... + } +) +``` +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`. -# 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. +## Run Workflow Conditionally -## numberOfExecutions Option +To run a workflow in another based on a condition, use when-then from the Workflows SDK. -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. +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). For example: -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} +```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" -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, +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], + }, + }) + }) + } +) ``` -The above scheduled job has the `numberOfExecutions` configuration set to `3`. +In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. -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. +# Long-Running Workflows +In this chapter, you’ll learn what a long-running workflow is and how to configure it. -# Example: Write Integration Tests for API Routes +## What is a Long-Running Workflow? -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. +When you execute a workflow, you wait until the workflow finishes execution to receive the output. -### Prerequisites +A long-running workflow is a workflow that continues its execution in the background. You don’t receive its output immediately. Instead, you subscribe to the workflow execution to listen to status changes and receive its result once the execution is finished. -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +### Why use Long-Running Workflows? -## Test a GET API Route +Long-running workflows are useful if: -Consider the following API route created at `src/api/custom/route.ts`: +- A task takes too long. For example, you're importing data from a CSV file. +- The workflow's steps wait for an external action to finish before resuming execution. For example, before you import the data from the CSV file, you wait until the import is confirmed by the user. -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +*** -export async function GET( - req: MedusaRequest, - res: MedusaResponse -){ - res.json({ - message: "Hello, World!", - }) -} -``` +## Configure Long-Running Workflows -To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: +A workflow is considered long-running if at least one step has its `async` configuration set to `true` and doesn't return a step response. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +For example, consider the following workflow and steps: -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/custom` - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` +```ts title="src/workflows/hello-world.ts" highlights={[["15"]]} collapsibleLines="1-11" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, + StepResponse, +} from "@medusajs/framework/workflows-sdk" -You use the `medusaIntegrationTestRunner` to write your tests. +const step1 = createStep("step-1", async () => { + return new StepResponse({}) +}) -You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: +const step2 = createStep( + { + name: "step-2", + async: true, + }, + async () => { + console.log("Waiting to be successful...") + } +) -- Have a code status `200`, -- Have a `message` property in the returned data. -- Have the value of the `message` property equal to `Hello, World!`. +const step3 = createStep("step-3", async () => { + return new StepResponse("Finished three steps") +}) -### Run Tests +const myWorkflow = createWorkflow( + "hello-world", + function () { + step1() + step2() + const message = step3() -Run the following command to run your tests: + return new WorkflowResponse({ + message, + }) +}) -```bash npm2yarn -npm run test:integration +export default myWorkflow ``` -If you don't have a `test:integration` 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 under the `src/integrations/http` directory. - -### Jest Timeout +The second step has in its configuration object `async` set to `true` and it doesn't return a step response. This indicates that this step is an asynchronous step. -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: +So, when you execute the `hello-world` workflow, it continues its execution in the background once it reaches the second step. -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +A workflow is also considered long-running if one of its steps has their `retryInterval` option set as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/retry-failed-steps/index.html.md). *** -## Test a POST API Route - -Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: - -```ts title="src/modules/hello/models/my-custom.ts" -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), -}) +## Change Step Status -export default MyCustom -``` +Once the workflow's execution reaches an async step, it'll wait in the background for the step to succeed or fail before it moves to the next step. -And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: +To fail or succeed a step, use the Workflow Engine Module's main service that is registered in the Medusa Container under the `Modules.WORKFLOW_ENGINE` (or `workflowsModuleService`) key. -```ts title="src/api/custom/route.ts" -// other imports... -import HelloModuleService from "../../../modules/hello/service" +### Retrieve Transaction ID -// ... +Before changing the status of a workflow execution's async step, you must have the execution's transaction ID. -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" - ) +When you execute the workflow, the object returned has a `transaction` property, which is an object that holds the details of the workflow execution's transaction. Use its `transactionId` to later change async steps' statuses: - const myCustom = await helloModuleService.createMyCustoms( - req.body - ) +```ts +const { transaction } = await myWorkflow(req.scope) + .run() - res.json({ - my_custom: myCustom, - }) -} +// use transaction.transactionId later ``` -This API route creates a new record of `MyCustom`. +### Change Step Status to Successful -To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: +The Workflow Engine Module's main service has a `setStepSuccess` method to set a step's status to successful. If you use it on a workflow execution's async step, the workflow continues execution to the next step. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} -// other imports... -import HelloModuleService from "../../src/modules/hello/service" +For example, consider the following step: -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... +```ts highlights={successStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" - describe("POST /custom", () => { - const id = "1" +type SetStepSuccessStepInput = { + transactionId: string +}; - it("Creates my custom", async () => { - - const response = await api.post( - `/custom`, - { - id, - name: "Test", - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("my_custom") - expect(response.data.my_custom).toEqual({ - id, - name: "Test", - created_at: expect.any(String), - updated_at: expect.any(String), - }) - }) - }) +export const setStepSuccessStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepSuccessStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + await workflowEngineService.setStepSuccess({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Done!"), + options: { + container, + }, }) - }, -}) + } +) ``` -This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. +In this step (which you use in a workflow other than the long-running workflow), you resolve the Workflow Engine Module's main service and set `step-2` of the previous workflow as successful. -The test passes if the response has: +The `setStepSuccess` method of the workflow engine's main service accepts as a parameter an object having the following properties: -- Status code `200`. -- A `my_custom` property in its data. -- Its `id` and `name` match the ones provided to the request. +- idempotencyKey: (\`object\`) The details of the workflow execution. -### Tear Down Created Record + - action: (\`invoke\` | \`compensate\`) If the step's compensation function is running, use \`compensate\`. Otherwise, use \`invoke\`. -To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. + - transactionId: (\`string\`) The ID of the workflow execution's transaction. -Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes + - stepId: (\`string\`) The ID of the step to change its status. This is the first parameter passed to \`createStep\` when creating the step. -So, add an `afterAll` hook in the `describe` block for `POST /custom`: + - workflowId: (\`string\`) The ID of the workflow. This is the first parameter passed to \`createWorkflow\` when creating the workflow. +- stepResponse: (\`StepResponse\`) Set the response of the step. This is similar to the response you return in a step's definition, but since the \`async\` step doesn't have a response, you set its response when changing its status. +- options: (\`Record\\`) Options to pass to the step. -```ts title="integration-tests/http/custom-routes.spec.ts" -// other imports... -import HelloModuleService from "../../src/modules/hello/service" + - container: (\`MedusaContainer\`) An instance of the Medusa Container -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // other tests... +### Change Step Status to Failed - describe("POST /custom", () => { - // ... - afterAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +The Workflow Engine Module's main service also has a `setStepFailure` method that changes a step's status to failed. It accepts the same parameter as `setStepSuccess`. - await helloModuleService.deleteMyCustoms(id) - }) - }) - }) - }, -}) -``` +After changing the async step's status to failed, the workflow execution fails and the compensation functions of previous steps are executed. -The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. +For example: -*** +```ts highlights={failureStatusHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" +import { + Modules, + TransactionHandlerType, +} from "@medusajs/framework/utils" +import { + StepResponse, + createStep, +} from "@medusajs/framework/workflows-sdk" -## Test a DELETE API Route +type SetStepFailureStepInput = { + transactionId: string +}; -Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: +export const setStepFailureStep = createStep( + "set-step-success-step", + async function ( + { transactionId }: SetStepFailureStepInput, + { container } + ) { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) -```ts title="src/api/custom/[id]/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import HelloModuleService from "../../../modules/hello/service" + await workflowEngineService.setStepFailure({ + idempotencyKey: { + action: TransactionHandlerType.INVOKE, + transactionId, + stepId: "step-2", + workflowId: "hello-world", + }, + stepResponse: new StepResponse("Failed!"), + options: { + container, + }, + }) + } +) +``` -export async function DELETE( - req: MedusaRequest, - res: MedusaResponse -) { - const helloModuleService: HelloModuleService = req.scope.resolve( - "helloModuleService" +You use this step in another workflow that changes the status of an async step in a long-running workflow's execution to failed. + +*** + +## Access Long-Running Workflow Status and Result + +To access the status and result of a long-running workflow execution, use the `subscribe` and `unsubscribe` methods of the Workflow Engine Module's main service. + +To retrieve the workflow execution's details at a later point, you must enable [storing the workflow's executions](https://docs.medusajs.com/learn/fundamentals/workflows/store-executions/index.html.md). + +For example: + +```ts title="src/api/workflows/route.ts" highlights={highlights} collapsibleLines="1-11" expandButtonLabel="Show Imports" +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import myWorkflow from "../../../workflows/hello-world" +import { + IWorkflowEngineService, +} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" + +export async function GET(req: MedusaRequest, res: MedusaResponse) { + const { transaction, result } = await myWorkflow(req.scope).run() + + const workflowEngineService = req.scope.resolve< + IWorkflowEngineService + >( + Modules.WORKFLOW_ENGINE ) - await helloModuleService.deleteMyCustoms(req.params.id) + const subscriptionOptions = { + workflowId: "hello-world", + transactionId: transaction.transactionId, + subscriberId: "hello-world-subscriber", + } - res.json({ - success: true, + await workflowEngineService.subscribe({ + ...subscriptionOptions, + subscriber: async (data) => { + if (data.eventType === "onFinish") { + console.log("Finished execution", data.result) + // unsubscribe + await workflowEngineService.unsubscribe({ + ...subscriptionOptions, + subscriberOrId: subscriptionOptions.subscriberId, + }) + } else if (data.eventType === "onStepFailure") { + console.log("Workflow failed", data.step) + } + }, }) + + res.send(result) } ``` -This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. +In the above example, you execute the long-running workflow `hello-world` and resolve the Workflow Engine Module's main service from the Medusa container. -To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: +### subscribe Method -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - // ... +The main service's `subscribe` method allows you to listen to changes in the workflow execution’s status. It accepts an object having three properties: - describe("DELETE /custom/:id", () => { - const id = "1" +- workflowId: (\`string\`) The name of the workflow. +- transactionId: (\`string\`) The ID of the workflow exection's transaction. The transaction's details are returned in the response of the workflow execution. +- subscriberId: (\`string\`) The ID of the subscriber. +- subscriber: (\`(data: \{ eventType: string, result?: any }) => Promise\\`) The function executed when the workflow execution's status changes. The function receives a data object. It has an \`eventType\` property, which you use to check the status of the workflow execution. - beforeAll(() => async () => { - const helloModuleService: HelloModuleService = getContainer().resolve( - "helloModuleService" - ) +If the value of `eventType` in the `subscriber` function's first parameter is `onFinish`, the workflow finished executing. The first parameter then also has a `result` property holding the workflow's output. - await helloModuleService.createMyCustoms({ - id, - name: "Test", - }) - }) +### unsubscribe Method - it("Deletes my custom", async () => { - const response = await api.delete( - `/custom/${id}` - ) +You can unsubscribe from the workflow using the workflow engine's `unsubscribe` method, which requires the same object parameter as the `subscribe` method. - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("success") - expect(response.data.success).toBeTruthy() - }) - }) - }) - }, -}) -``` +However, instead of the `subscriber` property, it requires a `subscriberOrId` property whose value is the same `subscriberId` passed to the `subscribe` method. -This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. +*** -In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: +## Example: Restaurant-Delivery Recipe -- Has a `200` status code. -- Has a `success` property in its data. -- The `success` property's value is true. +To find a full example of a long-running workflow, refer to the [restaurant-delivery recipe](https://docs.medusajs.com/resources/recipes/marketplace/examples/restaurant-delivery/index.html.md). -*** +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. -## Pass Headers in Test Requests -Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. +# Multiple Step Usage in Workflow -The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. +In this chapter, you'll learn how to use a step multiple times in a workflow. -### Pass Publishable API Key +## Problem Reusing a Step in a Workflow -For example, to pass a publishable API key in the header for a request to a `/store` route: +In some cases, you may need to use a step multiple times in the same workflow. -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - let pak: ApiKeyDTO - beforeAll(async () => { - pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "" - } - ] - } - })).result[0] - }) - describe("GET /custom", () => { - it("returns correct message", async () => { - const response = await api.get( - `/store/custom`, - { - headers: { - "x-publishable-api-key": pak.token - } - } - ) - - expect(response.status).toEqual(200) - expect(response.data).toHaveProperty("message") - expect(response.data.message).toEqual("Hello, World!") - }) - }) +Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: + +```ts +const useQueryGraphStep = createStep( + "use-query-graph" + // ... +) +``` + +This causes an error when you use the same step multiple times in a workflow, as it's registered in the workflow as two steps having the same ID: + +```ts +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], }) - }, -}) -jest.setTimeout(60 * 1000) + // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }) + } +) ``` -In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). +The next section explains how to fix this issue to use the same step multiple times in a workflow. -Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. +*** -### Send Authenticated Requests +## How to Use a Step Multiple Times in a Workflow? -If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. +When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. -For example: +Use the `config` method to change a step's ID for a single execution. -For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. +So, this is the correct way to write the example above: -### Admin User +```ts highlights={highlights} +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) -```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import jwt from "jsonwebtoken" + // ✓ No error occurs, the step has a different ID. + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }).config({ name: "fetch-customers" }) + } +) +``` -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const userModuleService = container.resolve("user") - - const user = await userModuleService.createUsers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "admin@medusa.js", - provider_metadata: { - password: "supersecret" - } - } - ], - app_metadata: { - user_id: user.id - } - }) - - const token = jwt.sign( - { - actor_id: user.id, - actor_type: "user", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` - }) - it("returns correct message", async () => { - const response = await api.get( - `/admin/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -### Customer User - -```ts title="integration-tests/http/custom-routes.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { ApiKeyDTO } from "@medusajs/framework/types" -import jwt from "jsonwebtoken" -import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" +The `config` method accepts an object with a `name` property. Its value is a new ID of the step to use for this execution only. -medusaIntegrationTestRunner({ - testSuite: ({ api, getContainer }) => { - describe("Custom endpoints", () => { - describe("GET /custom", () => { - const headers: Record = { - } - beforeEach(async () => { - const container = getContainer() - - const authModuleService = container.resolve("auth") - const customerModuleService = container.resolve("customer") - - const customer = await customerModuleService.createCustomers({ - email: "admin@medusa.js", - - }) - const authIdentity = await authModuleService.createAuthIdentities({ - provider_identities: [ - { - provider: "emailpass", - entity_id: "customer@medusa.js", - provider_metadata: { - password: "supersecret" - } - } - ], - app_metadata: { - user_id: customer.id - } - }) - - const token = jwt.sign( - { - actor_id: customer.id, - actor_type: "customer", - auth_identity_id: authIdentity.id, - }, - "supersecret", - { - expiresIn: "1d", - } - ) - - headers["authorization"] = `Bearer ${token}` +The first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. - const pak = (await createApiKeysWorkflow(getContainer()).run({ - input: { - api_keys: [ - { - type: "publishable", - title: "Test Key", - created_by: "" - } - ] - } - })).result[0] +# Store Workflow Executions - headers["x-publishable-api-key"] = pak.token - }) - it("returns correct message", async () => { - const response = await api.get( - `/store/customers/me/custom`, - { headers } - ) - - expect(response.status).toEqual(200) - }) - }) - }) - }, -}) +In this chapter, you'll learn how to store workflow executions in the database and access them later. -jest.setTimeout(60 * 1000) -``` +## Workflow Execution Retention -In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. +Medusa doesn't store your workflow's execution details by default. However, you can configure a workflow to keep its execution details stored in the database. -You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). +This is useful for auditing and debugging purposes. When you store a workflow's execution, you can view details around its steps, their states and their output. You can also check whether the workflow or any of its steps failed. +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -# Example: Write Integration Tests for Workflows +*** -In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. +## How to Store Workflow's Executions? ### Prerequisites -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## Write Integration Test for Workflow +- [Redis Workflow Engine must be installed and configured.](https://docs.medusajs.com/resources/architectural-modules/workflow-engine/redis/index.html.md) -Consider you have the following workflow defined at `src/workflows/hello-world.ts`: +`createWorkflow` from the Workflows SDK can accept an object as a first parameter to set the workflow's configuration. To enable storing a workflow's executions: -```ts title="src/workflows/hello-world.ts" -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +- Enable the `store` option. If your workflow is a [Long-Running Workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md), this option is enabled by default. +- Set the `retentionTime` option to the number of seconds that the workflow execution should be stored in the database. -const step1 = createStep("step-1", () => { - return new StepResponse("Hello, World!") -}) +For example: -export const helloWorldWorkflow = createWorkflow( - "hello-world-workflow", - () => { - const message = step1() +```ts highlights={highlights} +import { createStep, createWorkflow } from "@medusajs/framework/workflows-sdk" - return new WorkflowResponse(message) +const step1 = createStep( + { + name: "step-1" + }, + async () => { + console.log("Hello from step 1") } ) -``` - -To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: - -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { result } = await helloWorldWorkflow(getContainer()) - .run() - - expect(result).toEqual("Hello, World!") - }) - }) +export const helloWorkflow = createWorkflow( + { + name: "hello-workflow", + retentionTime: 99999, + store: true }, -}) - -jest.setTimeout(60 * 1000) + () => { + step1() + } +) ``` -You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. - -### Jest Timeout - -Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: - -```ts title="integration-tests/http/custom-routes.spec.ts" -// in your test's file -jest.setTimeout(60 * 1000) -``` +Whenever you execute the `helloWorkflow` now, its execution details will be stored in the database. *** -## Run Test - -Run the following command to run your tests: - -```bash npm2yarn -npm run test:integration -``` +## Retrieve Workflow Executions -If you don't have a `test:integration` 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). +You can view stored workflow executions from the Medusa Admin dashboard by going to Settings -> Workflows. -This runs your Medusa application and runs the tests available under the `integrations/http` directory. +When you execute a workflow, the returned object has a `transaction` property containing the workflow execution's transaction details: -*** +```ts +const { transaction } = await helloWorkflow(container).run() +``` -## Test That a Workflow Throws an Error +To retrieve a workflow's execution details from the database, resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method. -You might want to test that a workflow throws an error in certain cases. To test this: +For example, you can create a `GET` API Route at `src/workflows/[id]/route.ts` that retrieves a workflow execution for the specified transaction ID: -- Disable the `throwOnError` option when executing the workflow. -- Use the returned `errors` property to check what errors were thrown. +```ts title="src/workflows/[id]/route.ts" highlights={retrieveHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework"; +import { Modules } from "@medusajs/framework/utils"; -For example, if you have a step that throws this error: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { transaction_id } = req.params + + const workflowEngineService = req.scope.resolve( + Modules.WORKFLOW_ENGINE + ) -```ts title="src/workflows/hello-world.ts" -import { MedusaError } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" + const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ + transaction_id: transaction_id + }) -const step1 = createStep("step-1", () => { - throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") -}) + res.json({ + workflowExecution + }) +} ``` -You can write the following test to ensure that the workflow throws that error: +In the above example, you resolve the Workflow Engine Module from the container and use its `listWorkflowExecutions` method, passing the `transaction_id` as a filter to retrieve its workflow execution details. -```ts title="integration-tests/http/workflow.spec.ts" -import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { helloWorldWorkflow } from "../../src/workflows/hello-world" +A workflow execution object will be similar to the following: -medusaIntegrationTestRunner({ - testSuite: ({ getContainer }) => { - describe("Test hello-world workflow", () => { - it("returns message", async () => { - const { errors } = await helloWorldWorkflow(getContainer()) - .run({ - throwOnError: false - }) - - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].error.message).toBe("Item doesn't exist") - }) - }) +```json +{ + "workflow_id": "hello-workflow", + "transaction_id": "01JJC2T6AVJCQ3N4BRD1EB88SP", + "id": "wf_exec_01JJC2T6B3P76JD35F12QTTA78", + "execution": { + "state": "done", + "steps": {}, + "modelId": "hello-workflow", + "options": {}, + "metadata": {}, + "startedAt": 1737719880027, + "definition": {}, + "timedOutAt": null, + "hasAsyncSteps": false, + "transactionId": "01JJC2T6AVJCQ3N4BRD1EB88SP", + "hasFailedSteps": false, + "hasSkippedSteps": false, + "hasWaitingSteps": false, + "hasRevertedSteps": false, + "hasSkippedOnFailureSteps": false }, -}) - -jest.setTimeout(60 * 1000) + "context": { + "data": {}, + "errors": [] + }, + "state": "done", + "created_at": "2025-01-24T09:58:00.036Z", + "updated_at": "2025-01-24T09:58:00.046Z", + "deleted_at": null +} ``` -The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. - -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 +### Example: Check if Stored Workflow Execution Failed -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) +To check if a stored workflow execution failed, you can check its `state` property: -## Write Integration Test for Module +```ts +if (workflowExecution.state === "failed") { + return res.status(500).json({ + error: "Workflow failed" + }) +} +``` -Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: +Other state values include `done`, `invoking`, and `compensating`. -```ts title="src/modules/hello/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" -class HelloModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} +# Variable Manipulation in Workflows with transform -export default HelloModuleService -``` +In this chapter, you'll learn how to use `transform` from the Workflows SDK to manipulate variables in a workflow. -To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: +## Why Variable Manipulation isn't Allowed in Workflows -```ts title="src/modules/hello/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { HELLO_MODULE } from ".." -import HelloModuleService from "../service" -import MyCustom from "../models/my-custom" +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. -moduleIntegrationTestRunner({ - moduleName: HELLO_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/hello", - testSuite: ({ service }) => { - describe("HelloModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() +At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) +So, you can only pass variables as parameters to steps. But, in a workflow, you can't change a variable's value or, if the variable is an array, loop over its items. -jest.setTimeout(60 * 1000) -``` +Instead, use `transform` from the Workflows SDK. -You use the `moduleIntegrationTestRunner` function to add tests for the `hello` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. +Restrictions for variable manipulation is only applicable in a workflow's definition. You can still manipulate variables in your step's code. *** -## Run Test +## What is the transform Utility? -Run the following command to run your module integration tests: +`transform` creates a new variable as the result of manipulating other variables. -```bash npm2yarn -npm run test:integration:modules -``` +For example, consider you have two strings as the output of two steps: -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). +```ts +const str1 = step1() +const str2 = step2() +``` -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +To concatenate the strings, you create a new variable `str3` using the `transform` function: +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +// step imports... -# Workflow Timeout +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str1 = step1(input) + const str2 = step2(input) -In this chapter, you’ll learn how to set a timeout for workflows and steps. + const str3 = transform( + { str1, str2 }, + (data) => `${data.str1}${data.str2}` + ) -## What is a Workflow Timeout? + return new WorkflowResponse(str3) + } +) +``` -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. +`transform` accepts two parameters: -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. +1. The first parameter is an object of variables to manipulate. The object is passed as a parameter to `transform`'s second parameter function. +2. The second parameter is the function performing the variable manipulation. -### Timeout Doesn't Stop Step Execution +The value returned by the second parameter function is returned by `transform`. So, the `str3` variable holds the concatenated string. -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. +You can use the returned value in the rest of the workflow, either to pass it as an input to other steps or to return it in the workflow's response. *** -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. +## Example: Looping Over Array -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. +Use `transform` to loop over arrays to create another variable from the array's items. For example: -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +```ts collapsibleLines="1-7" expandButtonLabel="Show Imports" import { - createStep, createWorkflow, WorkflowResponse, + transform, } from "@medusajs/framework/workflows-sdk" +// step imports... + +type WorkflowInput = { + items: { + id: string + name: string + }[] +} + +const myWorkflow = createWorkflow( + "hello-world", + function ({ items }: WorkflowInput) { + const ids = transform( + { items }, + (data) => data.items.map((item) => item.id) + ) + + doSomethingStep(ids) -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. +This workflow receives an `items` array in its input. -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/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. +You use `transform` to create an `ids` variable, which is an array of strings holding the `id` of each item in the `items` array. -*** +You then pass the `ids` variable as a parameter to the `doSomethingStep`. -## Configure Step Timeout +*** -Alternatively, you can configure the timeout for a step rather than the entire workflow. +## Example: Creating a Date -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. +If you create a date with `new Date()` in a workflow's constructor function, Medusa evaluates the date's value when it creates the internal representation of the workflow, not when the workflow is executed. -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. +So, use `transform` instead to create a date variable with `new Date()`. For example: -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... +```ts +const myWorkflow = createWorkflow( + "hello-world", + () => { + const today = transform({}, () => new Date()) + + doSomethingStep(today) } ) ``` -This step's executions fail if they run longer than two seconds. +In this workflow, `today` is only evaluated when the workflow is executed. -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/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. +*** +## Caveats -# Commerce Modules +### Transform Evaluation -In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. +`transform`'s value is only evaluated if you pass its output to a step or in the workflow response. -A commerce module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. +For example, if you have the following workflow: -A commerce module also defines data models, representing tables in the database. Medusa's framework and tools allow you to extend these data models to add custom fields. +```ts +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str = transform( + { input }, + (data) => `${data.input.str1}${data.input.str2}` + ) -## Commerce Modules List + return new WorkflowResponse("done") + } +) +``` -*** +Since `str`'s value isn't used as a step's input or passed to `WorkflowResponse`, its value is never evaluated. -## How to Use Modules +### Data Validation -The Commerce Modules can be used in many use cases, including: +`transform` should only be used to perform variable or data manipulation. -- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. -- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. -- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. +If you want to perform some validation on the data, use a step or [when-then](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md) instead. +For example: -# API Key Module +```ts +// DON'T +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + const str = transform( + { input }, + (data) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } + ) + } +) -In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. +// DO +const validateHasStr1Step = createStep( + "validate-has-str1", + ({ input }) => { + if (!input.str1) { + throw new Error("Not allowed!") + } + } +) -Medusa has API-key related features available out-of-the-box through the API Key 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 API Key Module. +const myWorkflow = createWorkflow( + "hello-world", + function (input) { + validateHasStr1({ + input, + }) -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + // workflow continues its execution only if + // the step doesn't throw the error. + } +) +``` -## API Key Features -- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. -- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. -- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. -- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). +# Run Workflow Steps in Parallel -*** +In this chapter, you’ll learn how to run workflow steps in parallel. -## How to Use the API Key Module +## parallelize Utility Function -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. +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. -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. +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. For example: -```ts title="src/workflows/create-api-key.ts" highlights={highlights} -import { - createWorkflow, +```ts highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { + createWorkflow, WorkflowResponse, - createStep, - StepResponse, + parallelize, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createApiKeyStep = createStep( - "create-api-key", - async ({}, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" - const apiKey = await apiKeyModuleService.createApiKeys({ - title: "Publishable API key", - type: "publishable", - created_by: "user_123", - }) +interface WorkflowInput { + title: string +} - return new StepResponse({ apiKey }, apiKey.id) - }, - async (apiKeyId, { container }) => { - const apiKeyModuleService = container.resolve(Modules.API_KEY) +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) - await apiKeyModuleService.deleteApiKeys([apiKeyId]) - } -) + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) -export const createApiKeyWorkflow = createWorkflow( - "create-api-key", - () => { - const { apiKey } = createApiKeyStep() + const id = product.id + const refetchedProduct = getProductStep(product.id) - return new WorkflowResponse({ - apiKey, - }) - } + return new WorkflowResponse(refetchedProduct) + } ) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +The `parallelize` function accepts the steps to run in parallel as a parameter. -### API Route +It returns an array of the steps' results in the same order they're passed to the `parallelize` function. -```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 { createApiKeyWorkflow } from "../../workflows/create-api-key" +So, `prices` is the result of `createPricesStep`, and `productSalesChannel` is the result of `attachProductToSalesChannelStep`. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createApiKeyWorkflow(req.scope) - .run() - res.send(result) -} -``` +# Retry Failed Steps -### Subscriber +In this chapter, you’ll learn how to configure steps to allow retrial on failure. -```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 { createApiKeyWorkflow } from "../workflows/create-api-key" +## Configure a Step’s Retrial -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createApiKeyWorkflow(container) - .run() +By default, when an error occurs in a step, the step and the workflow fail, and the execution stops. - console.log(result) -} +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. -export const config: SubscriberConfig = { - event: "user.created", -} -``` +For example: -### Scheduled Job +```ts title="src/workflows/hello-world.ts" highlights={[["10"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createApiKeyWorkflow } from "../workflows/create-api-key" +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + }, + async () => { + console.log("Executing step 1") -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createApiKeyWorkflow(container) - .run() + throw new Error("Oops! Something happened.") + } +) - console.log(result) -} +const myWorkflow = createWorkflow( + "hello-world", + function () { + const str1 = step1() -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +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 -# Cart Module +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. -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. +For example: -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. +```ts title="src/workflows/hello-world.ts" highlights={[["5"]]} +const step1 = createStep( + { + name: "step-1", + maxRetries: 2, + retryInterval: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +### Interval Changes Workflow to Long-Running -## Cart Features +By setting `retryInterval` on a step, a workflow becomes a [long-running workflow](https://docs.medusajs.com/learn/fundamentals/workflows/long-running-workflow/index.html.md) that runs asynchronously in the background. So, you won't receive its result or errors immediately when you execute the workflow. -- [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. +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). -*** -## How to Use the Cart Module +# Workflow Hooks -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. +In this chapter, you'll learn what a workflow hook is and how to consume them. -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. +## What is a Workflow Hook? -For example: +A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. -```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" +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. -const createCartStep = createStep( - "create-cart", - async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. - 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, - }, - ], - }) +You want to perform a custom action during a workflow's execution, such as when a product is created. - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) +*** - await cartModuleService.deleteCarts([cartId]) - } -) +## How to Consume a Hook? -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() +A workflow has a special `hooks` property which is an object that holds its hooks. - return new WorkflowResponse({ - cart, - }) +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 } ) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +The `productsCreated` hook is available on the workflow's `hooks` property by its name. -### API Route +You invoke the hook, passing a step function (the hook handler) as a parameter. -```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" +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. -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(req.scope) - .run() +A hook can have only one handler. - res.send(result) -} +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.productCreated( + async ({ productId }, { container }) => { + // TODO perform an action + + return new StepResponse(undefined, { ids }) + }, + async ({ ids }, { container }) => { + // undo the performed action + } +) ``` -### Subscriber +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. -```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" +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - console.log(result) -} +### Additional Data Property -export const config: SubscriberConfig = { - event: "user.created", -} +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 + } +) ``` -### Scheduled Job +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" +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). -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() +### Pass Additional Data to Workflow - console.log(result) -} +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, +```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", + }, + }, + }) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +Your hook handler then receives that passed data in the `additional_data` object. -*** +# Workflow Timeout -# Customer Module +In this chapter, you’ll learn how to set a timeout for workflows and steps. -In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. +## What is a Workflow Timeout? -Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +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. -## Customer Features +### Timeout Doesn't Stop Step Execution -- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. -- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). +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. *** -## How to Use the Customer Module +## Configure Workflow Timeout -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. +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. -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. +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/create-customer.ts" highlights={highlights} +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" import { - createWorkflow, + createStep, + createWorkflow, WorkflowResponse, - createStep, - StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCustomerStep = createStep( - "create-customer", - async ({}, { container }) => { - const customerModuleService = container.resolve(Modules.CUSTOMER) - - const customer = await customerModuleService.createCustomers({ - first_name: "Peter", - last_name: "Hayes", - email: "peter.hayes@example.com", - }) - - return new StepResponse({ customer }, customer.id) - }, - async (customerId, { container }) => { - if (!customerId) { - return - } - const customerModuleService = container.resolve(Modules.CUSTOMER) - await customerModuleService.deleteCustomers([customerId]) +const step1 = createStep( + "step-1", + async () => { + // ... } ) -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow - return new WorkflowResponse({ - customer, - }) - } -) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +This workflow's executions fail if they run longer than two seconds. -### API Route +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/access-workflow-errors/index.html.md). The error’s name is `TransactionTimeoutError`. -```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 { createCustomerWorkflow } from "../../workflows/create-customer" +*** -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCustomerWorkflow(req.scope) - .run() +## Configure Step Timeout - res.send(result) -} +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 () => { + // ... + } +) ``` -### Subscriber +This step's executions fail if they run longer than two seconds. -```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 { createCustomerWorkflow } from "../workflows/create-customer" +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/access-workflow-errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(container) - .run() - console.log(result) -} +# Example: Write Integration Tests for API Routes -export const config: SubscriberConfig = { - event: "user.created", -} -``` +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. -### Scheduled Job +### Prerequisites -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCustomerWorkflow } from "../workflows/create-customer" +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(container) - .run() +## Test a GET API Route - console.log(result) -} +Consider the following API route created at `src/api/custom/route.ts`: -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +){ + res.json({ + message: "Hello, World!", + }) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - +To write an integration test that tests this API route, create the file `integration-tests/http/custom-routes.spec.ts` with the following content: -# Auth Module +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={getHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -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. +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/custom` + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) + }) + }, +}) -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. +jest.setTimeout(60 * 1000) +``` -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +You use the `medusaIntegrationTestRunner` to write your tests. -## Auth Features +You add a single test that sends a `GET` request to `/custom` using the `api.get` method. For the test to pass, the response is expected to: -- [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. +- Have a code status `200`, +- Have a `message` property in the returned data. +- Have the value of the `message` property equal to `Hello, World!`. -*** +### Run Tests -## How to Use the Auth Module +Run the following command to run your tests: -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. +```bash npm2yarn +npm run test:integration +``` -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. +If you don't have a `test:integration` 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). -For example: +This runs your Medusa application and runs the tests available under the `src/integrations/http` directory. -```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" +### Jest Timeout -type Input = { - req: MedusaRequest -} +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) +```ts title="integration-tests/http/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) +``` - 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" - ) - } +## Test a POST API Route - return new StepResponse({ authIdentity }, authIdentity?.id) - }, - async (authIdentityId, { container }) => { - if (!authIdentityId) { - return - } - - const authModuleService = container.resolve(Modules.AUTH) +Suppose you have a `hello` module whose main service extends the service factory, and that has the following model: - await authModuleService.deleteAuthIdentities([authIdentityId]) - } -) +```ts title="src/modules/hello/models/my-custom.ts" +import { model } from "@medusajs/framework/utils" -export const authenticateUserWorkflow = createWorkflow( - "authenticate-user", - (input: Input) => { - const { authIdentity } = authenticateUserStep(input) +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), +}) - return new WorkflowResponse({ - authIdentity, - }) - } -) +export default MyCustom ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +And consider that the file `src/api/custom/route.ts` defines another route handler for `POST` requests: -```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" +```ts title="src/api/custom/route.ts" +// other imports... +import HelloModuleService from "../../../modules/hello/service" -export async function GET( +// ... + +export async function POST( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" + ) - res.send(result) + const myCustom = await helloModuleService.createMyCustoms( + req.body + ) + + res.json({ + my_custom: myCustom, + }) } ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** +This API route creates a new record of `MyCustom`. -## Configure Auth Module +To write tests for this API route, add the following at the end of the `testSuite` function in `integration-tests/http/custom-routes.spec.ts`: -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. +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={postHighlights} +// other imports... +import HelloModuleService from "../../src/modules/hello/service" -*** +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... -## Providers + describe("POST /custom", () => { + const id = "1" -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - -*** - - -# 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. - -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 + it("Creates my custom", async () => { + + const response = await api.post( + `/custom`, + { + id, + name: "Test", + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("my_custom") + expect(response.data.my_custom).toEqual({ + id, + name: "Test", + created_at: expect.any(String), + updated_at: expect.any(String), + }) + }) + }) + }) + }, +}) +``` -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. +This adds a test for the `POST /custom` API route. It uses `api.post` to send the POST request. The `api.post` method accepts as a second parameter the data to pass in the request body. -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. +The test passes if the response has: -For example: +- Status code `200`. +- A `my_custom` property in its data. +- Its `id` and `name` match the ones provided to the request. -```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" +### Tear Down Created Record -const retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) +To ensure consistency in the database for the rest of the tests after the above test is executed, utilize [Jest's setup and teardown hooks](https://jestjs.io/docs/setup-teardown) to delete the created record. - const currency = await currencyModuleService - .retrieveCurrency("usd") +Use the `getContainer` function passed as a parameter to the `testSuite` function to resolve a service and use it for setup or teardown purposes - return new StepResponse({ currency }) - } -) +So, add an `afterAll` hook in the `describe` block for `POST /custom`: -type Input = { - price: number -} +```ts title="integration-tests/http/custom-routes.spec.ts" +// other imports... +import HelloModuleService from "../../src/modules/hello/service" -export const retrievePriceWithCurrency = createWorkflow( - "create-currency", - (input: Input) => { - const { currency } = retrieveCurrencyStep() +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // other tests... - const formattedPrice = transform({ - input, - currency, - }, (data) => { - return `${data.currency.symbol}${data.input.price}` - }) + describe("POST /custom", () => { + // ... + afterAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) - return new WorkflowResponse({ - formattedPrice, + await helloModuleService.deleteMyCustoms(id) + }) + }) }) - } -) + }, +}) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +The `afterAll` hook resolves the `HelloModuleService` and use its `deleteMyCustoms` to delete the record created by the test. -### 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" +## Test a DELETE API Route -export async function GET( +Consider a `/custom/:id` API route created at `src/api/custom/[id]/route.ts`: + +```ts title="src/api/custom/[id]/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import HelloModuleService from "../../../modules/hello/service" + +export async function DELETE( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) + const helloModuleService: HelloModuleService = req.scope.resolve( + "helloModuleService" + ) - res.send(result) + await helloModuleService.deleteMyCustoms(req.params.id) + + res.json({ + success: true, + }) } ``` -### Subscriber +This API route accepts an ID path parameter, and uses the `HelloModuleService` to delete a `MyCustom` record by that ID. -```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" +To add tests for this API route, add the following to `integration-tests/http/custom-routes.spec.ts`: -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={deleteHighlights} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + // ... - console.log(result) -} + describe("DELETE /custom/:id", () => { + const id = "1" -export const config: SubscriberConfig = { - event: "user.created", -} -``` + beforeAll(() => async () => { + const helloModuleService: HelloModuleService = getContainer().resolve( + "helloModuleService" + ) -### Scheduled Job + await helloModuleService.createMyCustoms({ + id, + name: "Test", + }) + }) -```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" + it("Deletes my custom", async () => { + const response = await api.delete( + `/custom/${id}` + ) -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("success") + expect(response.data.success).toBeTruthy() + }) + }) }) - - 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). - -*** - - -# 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. - -Medusa has fulfillment related features available out-of-the-box through the Fulfillment 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 Fulfillment Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +This adds a new test for the `DELETE /custom/:id` API route. You use the `beforeAll` hook to create a `MyCustom` record using the `HelloModuleService`. -## Fulfillment Features +In the test, you use the `api.delete` method to send a `DELETE` request to `/custom/:id`. The test passes if the response: -- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. -- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. -- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. -- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. +- Has a `200` status code. +- Has a `success` property in its data. +- The `success` property's value is true. *** -## How to Use the Fulfillment Module +## Pass Headers in Test Requests -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. +Some requests require passing headers. For example, all routes prefixed with `/store` must pass a publishable API key in the header. -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. +The `get`, `post`, and `delete` methods accept an optional third parameter that you can pass a `headers` property to, whose value is an object of headers to pass in the request. -For example: +### Pass Publishable API Key -```ts title="src/workflows/create-fulfillment.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +For example, to pass a publishable API key in the header for a request to a `/store` route: -const createFulfillmentStep = createStep( - "create-fulfillment", - async ({}, { container }) => { - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={headersHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" - const fulfillment = await fulfillmentModuleService.createFulfillment({ - location_id: "loc_123", - provider_id: "webshipper", - delivery_address: { - country_code: "us", - city: "Strongsville", - address_1: "18290 Royalton Rd", - }, - items: [ - { - title: "Shirt", - sku: "SHIRT", - quantity: 1, - barcode: "123456", - }, - ], - labels: [], - order: {}, +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + let pak: ApiKeyDTO + beforeAll(async () => { + pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "" + } + ] + } + })).result[0] + }) + describe("GET /custom", () => { + it("returns correct message", async () => { + const response = await api.get( + `/store/custom`, + { + headers: { + "x-publishable-api-key": pak.token + } + } + ) + + expect(response.status).toEqual(200) + expect(response.data).toHaveProperty("message") + expect(response.data.message).toEqual("Hello, World!") + }) + }) }) - - return new StepResponse({ fulfillment }, fulfillment.id) }, - async (fulfillmentId, { container }) => { - if (!fulfillmentId) { - return - } - const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - - await fulfillmentModuleService.deleteFulfillment(fulfillmentId) - } -) - -export const createFulfillmentWorkflow = createWorkflow( - "create-fulfillment", - () => { - const { fulfillment } = createFulfillmentStep() +}) - return new WorkflowResponse({ - fulfillment, - }) - } -) +jest.setTimeout(60 * 1000) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +In your test suit, you add a `beforeAll` hook to create a publishable API key before the tests run. To create the API key, you can use the `createApiKeysWorkflow` or the [API Key Module's service](https://docs.medusajs.com/resources/commerce-modules/api-key/index.html.md). -### API Route +Then, in the test, you pass an object as the last parameter to `api.get` with a `headers` property. The `headers` property is an object with the key `x-publishable-api-key` and the value of the API key's token. -```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 { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" +### Send Authenticated Requests -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createFulfillmentWorkflow(req.scope) - .run() +If your custom route is accessible by authenticated users only, such as routes prefixed by `/admin` or `/store/customers/me`, you can create a test customer or user, generate a JWT token for them, and pass the token in the request's Authorization header. - res.send(result) -} -``` +For example: -### Subscriber +For custom actor types, you only need to change the `actorType` value in the `jwt.sign` method. -```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 { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" +### Admin User -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createFulfillmentWorkflow(container) - .run() +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={adminHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import jwt from "jsonwebtoken" - console.log(result) -} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const userModuleService = container.resolve("user") + + const user = await userModuleService.createUsers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "admin@medusa.js", + provider_metadata: { + password: "supersecret" + } + } + ], + app_metadata: { + user_id: user.id + } + }) + + const token = jwt.sign( + { + actor_id: user.id, + actor_type: "user", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` + }) + it("returns correct message", async () => { + const response = await api.get( + `/admin/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, +}) -export const config: SubscriberConfig = { - event: "user.created", -} +jest.setTimeout(60 * 1000) ``` -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" +### Customer User -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createFulfillmentWorkflow(container) - .run() +```ts title="integration-tests/http/custom-routes.spec.ts" highlights={customerHighlights} +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { ApiKeyDTO } from "@medusajs/framework/types" +import jwt from "jsonwebtoken" +import { createApiKeysWorkflow } from "@medusajs/medusa/core-flows" - console.log(result) -} +medusaIntegrationTestRunner({ + testSuite: ({ api, getContainer }) => { + describe("Custom endpoints", () => { + describe("GET /custom", () => { + const headers: Record = { + } + beforeEach(async () => { + const container = getContainer() + + const authModuleService = container.resolve("auth") + const customerModuleService = container.resolve("customer") + + const customer = await customerModuleService.createCustomers({ + email: "admin@medusa.js", + + }) + const authIdentity = await authModuleService.createAuthIdentities({ + provider_identities: [ + { + provider: "emailpass", + entity_id: "customer@medusa.js", + provider_metadata: { + password: "supersecret" + } + } + ], + app_metadata: { + user_id: customer.id + } + }) + + const token = jwt.sign( + { + actor_id: customer.id, + actor_type: "customer", + auth_identity_id: authIdentity.id, + }, + "supersecret", + { + expiresIn: "1d", + } + ) + + headers["authorization"] = `Bearer ${token}` -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). + const pak = (await createApiKeysWorkflow(getContainer()).run({ + input: { + api_keys: [ + { + type: "publishable", + title: "Test Key", + created_by: "" + } + ] + } + })).result[0] -*** - -## Configure Fulfillment Module - -The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. - -*** - - -# Inventory Module + headers["x-publishable-api-key"] = pak.token + }) + it("returns correct message", async () => { + const response = await api.get( + `/store/customers/me/custom`, + { headers } + ) + + expect(response.status).toEqual(200) + }) + }) + }) + }, +}) -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. +jest.setTimeout(60 * 1000) +``` -Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory Module. +In the test suite, you add a `beforeEach` hook that creates a user or customer, an auth identity, and generates a JWT token for them. The JWT token is then set in the `Authorization` header of the request. -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +You also create and pass a publishable API key in the header for the customer as it's required for requests to `/store` routes. Learn more in [this section](#pass-publishable-api-key). -## Inventory Features -- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. -- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. -- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. -- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. -- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +# Example: Write Integration Tests for Workflows -*** +In this chapter, you'll learn how to write integration tests for workflows using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framwork. -## How to Use the Inventory Module +### Prerequisites -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. +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/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. +## Write Integration Test for Workflow -For example: +Consider you have the following workflow defined at `src/workflows/hello-world.ts`: -```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, +```ts title="src/workflows/hello-world.ts" +import { + createWorkflow, createStep, StepResponse, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createInventoryItemStep = createStep( - "create-inventory-item", - async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, - }) - - return new StepResponse({ inventoryItem }, inventoryItem.id) - }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { - return - } - const inventoryModuleService = container.resolve(Modules.INVENTORY) - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) - } -) +const step1 = createStep("step-1", () => { + return new StepResponse("Hello, World!") +}) -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", +export const helloWorldWorkflow = createWorkflow( + "hello-world-workflow", () => { - const { inventoryItem } = createInventoryItemStep() + const message = step1() - return new WorkflowResponse({ - inventoryItem, - }) + return new WorkflowResponse(message) } ) ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +To write a test for this workflow, create the file `integration-tests/http/workflow.spec.ts` with the following content: -### API Route +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -```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 { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { result } = await helloWorldWorkflow(getContainer()) + .run() -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createInventoryItemWorkflow(req.scope) - .run() + expect(result).toEqual("Hello, World!") + }) + }) + }, +}) - res.send(result) -} +jest.setTimeout(60 * 1000) ``` -### 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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +You use the `medusaIntegrationTestRunner` to write an integration test for the workflow. The test pases if the workflow returns the string `"Hello, World!"`. -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) - .run() +### Jest Timeout - console.log(result) -} +Since your tests connect to the database and perform actions that require more time than the typical tests, make sure to increase the timeout in your test: -export const config: SubscriberConfig = { - event: "user.created", -} +```ts title="integration-tests/http/custom-routes.spec.ts" +// in your test's file +jest.setTimeout(60 * 1000) ``` -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +*** -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createInventoryItemWorkflow(container) - .run() +## Run Test - console.log(result) -} +Run the following command to run your tests: -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} +```bash npm2yarn +npm run test:integration ``` -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +If you don't have a `test:integration` 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 under the `integrations/http` directory. *** +## Test That a Workflow Throws an Error -# Order Module +You might want to test that a workflow throws an error in certain cases. To test this: -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. +- Disable the `throwOnError` option when executing the workflow. +- Use the returned `errors` property to check what errors were thrown. -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. +For example, if you have a step that throws this error: -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). +```ts title="src/workflows/hello-world.ts" +import { MedusaError } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" -## Order Features +const step1 = createStep("step-1", () => { + throw new MedusaError(MedusaError.Types.NOT_FOUND, "Item doesn't exist") +}) +``` -- [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. +You can write the following test to ensure that the workflow throws that error: -*** +```ts title="integration-tests/http/workflow.spec.ts" +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { helloWorldWorkflow } from "../../src/workflows/hello-world" -## How to Use the Order Module +medusaIntegrationTestRunner({ + testSuite: ({ getContainer }) => { + describe("Test hello-world workflow", () => { + it("returns message", async () => { + const { errors } = await helloWorldWorkflow(getContainer()) + .run({ + throwOnError: false + }) -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. + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].error.message).toBe("Item doesn't exist") + }) + }) + }, +}) -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. +jest.setTimeout(60 * 1000) +``` -For example: +The `errors` property contains an array of errors thrown during the execution of the workflow. Each error item has an `error` object, being the error thrown. -```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" +If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. -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", - }) +# Example: Integration Tests for a Module - return new StepResponse({ draftOrder }, draftOrder.id) - }, - async (draftOrderId, { container }) => { - if (!draftOrderId) { - return - } - const orderModuleService = container.resolve(Modules.ORDER) +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. - await orderModuleService.deleteOrders([draftOrderId]) - } -) +### Prerequisites -export const createDraftOrderWorkflow = createWorkflow( - "create-draft-order", - () => { - const { draftOrder } = createDraftOrderStep() +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - return new WorkflowResponse({ - draftOrder, - }) +## Write Integration Test for Module + +Consider a `hello` module with a `HelloModuleService` that has a `getMessage` method: + +```ts title="src/modules/hello/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" + +class HelloModuleService extends MedusaService({ + MyCustom, +}){ + getMessage(): string { + return "Hello, World!" } -) +} + +export default HelloModuleService ``` -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: +To create an integration test for the method, create the file `src/modules/hello/__tests__/service.spec.ts` with the following content: -### API Route +```ts title="src/modules/hello/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { HELLO_MODULE } from ".." +import HelloModuleService from "../service" +import MyCustom from "../models/my-custom" -```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" +moduleIntegrationTestRunner({ + moduleName: HELLO_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/hello", + testSuite: ({ service }) => { + describe("HelloModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createDraftOrderWorkflow(req.scope) - .run() + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) - res.send(result) -} +jest.setTimeout(60 * 1000) ``` -### Subscriber +You use the `moduleIntegrationTestRunner` function to add tests for the `hello` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. -```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() +## Run Test - console.log(result) -} +Run the following command to run your module integration tests: -export const config: SubscriberConfig = { - event: "user.created", -} +```bash npm2yarn +npm run test:integration:modules ``` -### Scheduled Job +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). -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createDraftOrderWorkflow } from "../workflows/create-draft-order" +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createDraftOrderWorkflow(container) - .run() - console.log(result) -} +# Commerce Modules -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +In this section of the documentation, you'll find guides and references related to Medusa's commerce modules. -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +A commerce module provides features for a commerce domain within its service. The Medusa application exposes these features in its API routes to clients. + +A commerce module also defines data models, representing tables in the database. Medusa's framework and tools allow you to extend these data models to add custom fields. + +## Commerce Modules List *** +## How to Use Modules + +The Commerce Modules can be used in many use cases, including: -# Pricing Module +- Medusa Application: The Medusa application uses the Commerce Modules to expose commerce features through the REST APIs. +- Serverless Application: Use the Commerce Modules in a serverless application, such as a Next.js application, without having to manage a fully-fledged ecommerce system. You can use it by installing it in your Node.js project as an NPM package. +- Node.js Application: Use the Commerce Modules in any Node.js application by installing it with NPM. -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. -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. +# API Key Module + +In this section of the documentation, you will find resources to learn more about the API Key Module and how to use it in your application. + +Medusa has API-key related features available out-of-the-box through the API Key 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 API Key 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 +## API Key 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 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. +- [API Key Types and Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts/index.html.md): Manage API keys in your store. You can create both publishable and secret API keys for different use cases. +- [Token Verification](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#token-verification/index.html.md): Verify tokens of secret API keys to authenticate users or actions. +- [Revoke Keys](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/api-key/concepts#api-key-expiration/index.html.md): Revoke keys to disable their use permanently. +- Roll API Keys: Roll API keys by [revoking](https://docs.medusajs.com/references/api-key/revoke/index.html.md) a key then [re-creating it](https://docs.medusajs.com/references/api-key/createApiKeys/index.html.md). *** -## How to Use the Pricing Module +## How to Use the API Key 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. @@ -15204,7 +14043,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-api-key.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15213,46 +14052,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPriceSetStep = createStep( - "create-price-set", +const createApiKeyStep = createStep( + "create-api-key", async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) + const apiKeyModuleService = container.resolve(Modules.API_KEY) - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - { - amount: 500, - currency_code: "USD", - }, - { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, - }, - ], + const apiKey = await apiKeyModuleService.createApiKeys({ + title: "Publishable API key", + type: "publishable", + created_by: "user_123", }) - return new StepResponse({ priceSet }, priceSet.id) + return new StepResponse({ apiKey }, apiKey.id) }, - async (priceSetId, { container }) => { - if (!priceSetId) { - return - } - const pricingModuleService = container.resolve(Modules.PRICING) + async (apiKeyId, { container }) => { + const apiKeyModuleService = container.resolve(Modules.API_KEY) - await pricingModuleService.deletePriceSets([priceSetId]) + await apiKeyModuleService.deleteApiKeys([apiKeyId]) } ) -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", +export const createApiKeyWorkflow = createWorkflow( + "create-api-key", () => { - const { priceSet } = createPriceSetStep() + const { apiKey } = createApiKeyStep() return new WorkflowResponse({ - priceSet, + apiKey, }) } ) @@ -15267,13 +14093,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPriceSetWorkflow } from "../../workflows/create-price-set" +import { createApiKeyWorkflow } from "../../workflows/create-api-key" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPriceSetWorkflow(req.scope) + const { result } = await createApiKeyWorkflow(req.scope) .run() res.send(result) @@ -15287,13 +14113,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPriceSetWorkflow } from "../workflows/create-price-set" +import { createApiKeyWorkflow } from "../workflows/create-api-key" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createApiKeyWorkflow(container) .run() console.log(result) @@ -15308,12 +14134,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 { createApiKeyWorkflow } from "../workflows/create-api-key" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPriceSetWorkflow(container) + const { result } = await createApiKeyWorkflow(container) .run() console.log(result) @@ -15330,23 +14156,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Product Module +# Auth Module -In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application. +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 product related features available out-of-the-box through the Product 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 Product Module. +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). -## Product Features +## Auth Features -- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. -- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. -- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. +- [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 Product Module +## 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. @@ -15354,57 +14181,67 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-product.ts" highlights={highlights} +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" -const createProductStep = createStep( - "create-product", - async ({}, { container }) => { - const productService = container.resolve(Modules.PRODUCT) +type Input = { + req: MedusaRequest +} - const product = await productService.createProducts({ - title: "Medusa Shirt", - options: [ - { - title: "Color", - values: ["Black", "White"], - }, - ], - variants: [ - { - title: "Black Shirt", - options: { - Color: "Black", - }, - }, - ], - }) +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) - return new StepResponse({ product }, product.id) - }, - async (productId, { container }) => { - if (!productId) { + 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 productService = container.resolve(Modules.PRODUCT) + + const authModuleService = container.resolve(Modules.AUTH) - await productService.deleteProducts([productId]) + await authModuleService.deleteAuthIdentities([authIdentityId]) } ) -export const createProductWorkflow = createWorkflow( - "create-product", - () => { - const { product } = createProductStep() +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) return new WorkflowResponse({ - product, + authIdentity, }) } ) @@ -15412,94 +14249,61 @@ export const createProductWorkflow = createWorkflow( 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" +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createProductWorkflow } from "../../workflows/create-product" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createProductWorkflow(req.scope) - .run() + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) 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 { createProductWorkflow } from "../workflows/create-product" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createProductWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -### Scheduled Job +*** -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createProductWorkflow } from "../workflows/create-product" +## Configure Auth Module -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createProductWorkflow(container) - .run() +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. - console.log(result) -} +*** -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` +## Providers -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). +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 +# Cart 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. +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 payment related features available out-of-the-box through the Payment 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 Payment 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). -## Payment Features +## Cart Features -- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. -- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. -- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. -- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. +- [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 Payment 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. @@ -15507,7 +14311,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} +```ts title="src/workflows/create-cart.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15516,35 +14320,45 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPaymentCollectionStep = createStep( - "create-payment-collection", +const createCartStep = createStep( + "create-cart", async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) + const cartModuleService = container.resolve(Modules.CART) - const paymentCollection = await paymentModuleService.createPaymentCollections({ + const cart = await cartModuleService.createCarts({ currency_code: "usd", - amount: 5000, + shipping_address: { + address_1: "1512 Barataria Blvd", + country_code: "us", + }, + items: [ + { + title: "Shirt", + unit_price: 1000, + quantity: 1, + }, + ], }) - return new StepResponse({ paymentCollection }, paymentCollection.id) + return new StepResponse({ cart }, cart.id) }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { + async (cartId, { container }) => { + if (!cartId) { return } - const paymentModuleService = container.resolve(Modules.PAYMENT) + const cartModuleService = container.resolve(Modules.CART) - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + await cartModuleService.deleteCarts([cartId]) } ) -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", +export const createCartWorkflow = createWorkflow( + "create-cart", () => { - const { paymentCollection } = createPaymentCollectionStep() + const { cart } = createCartStep() return new WorkflowResponse({ - paymentCollection, + cart, }) } ) @@ -15559,13 +14373,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" +import { createCartWorkflow } from "../../workflows/create-cart" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPaymentCollectionWorkflow(req.scope) + const { result } = await createCartWorkflow(req.scope) .run() res.send(result) @@ -15579,13 +14393,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createCartWorkflow } from "../workflows/create-cart" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -15600,12 +14414,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" +import { createCartWorkflow } from "../workflows/create-cart" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPaymentCollectionWorkflow(container) + const { result } = await createCartWorkflow(container) .run() console.log(result) @@ -15621,38 +14435,23 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure Payment Module - -The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. - -*** - -## Providers - -Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. - -*** - -# Region Module +# Customer 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. +In this section of the documentation, you will find resources to learn more about the Customer Module and how to use it in your application. -Medusa has region related features available out-of-the-box through the Region 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 Region Module. +Medusa has customer related features available out-of-the-box through the Customer 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 Customer Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -*** - -## Region Features +## Customer Features -- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. -- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. -- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. +- [Customer Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/customer-accounts/index.html.md): Store and manage guest and registered customers in your store. +- [Customer Organization](https://docs.medusajs.com/references/customer/models/index.html.md): Organize customers into groups. This has a lot of benefits and supports many use cases, such as provide discounts for specific customer groups using the [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md). *** -## How to Use Region Module's Service +## How to Use the Customer 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. @@ -15660,7 +14459,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-region.ts" highlights={highlights} +```ts title="src/workflows/create-customer.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15669,35 +14468,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createRegionStep = createStep( - "create-region", +const createCustomerStep = createStep( + "create-customer", async ({}, { container }) => { - const regionModuleService = container.resolve(Modules.REGION) + const customerModuleService = container.resolve(Modules.CUSTOMER) - const region = await regionModuleService.createRegions({ - name: "Europe", - currency_code: "eur", + const customer = await customerModuleService.createCustomers({ + first_name: "Peter", + last_name: "Hayes", + email: "peter.hayes@example.com", }) - return new StepResponse({ region }, region.id) + return new StepResponse({ customer }, customer.id) }, - async (regionId, { container }) => { - if (!regionId) { + async (customerId, { container }) => { + if (!customerId) { return } - const regionModuleService = container.resolve(Modules.REGION) + const customerModuleService = container.resolve(Modules.CUSTOMER) - await regionModuleService.deleteRegions([regionId]) + await customerModuleService.deleteCustomers([customerId]) } ) -export const createRegionWorkflow = createWorkflow( - "create-region", +export const createCustomerWorkflow = createWorkflow( + "create-customer", () => { - const { region } = createRegionStep() + const { customer } = createCustomerStep() return new WorkflowResponse({ - region, + customer, }) } ) @@ -15712,13 +14512,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createRegionWorkflow } from "../../workflows/create-region" +import { createCustomerWorkflow } from "../../workflows/create-customer" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createRegionWorkflow(req.scope) + const { result } = await createCustomerWorkflow(req.scope) .run() res.send(result) @@ -15732,13 +14532,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createRegionWorkflow } from "../workflows/create-region" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createRegionWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -15753,12 +14553,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createRegionWorkflow } from "../workflows/create-region" +import { createCustomerWorkflow } from "../workflows/create-customer" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createRegionWorkflow(container) + const { result } = await createCustomerWorkflow(container) .run() console.log(result) @@ -15775,22 +14575,25 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module +# Inventory 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. +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. -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. +Medusa has inventory related features available out-of-the-box through the Inventory 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 Inventory 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. +## Inventory Features + +- [Inventory Items Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md): Store and manage inventory of any stock-kept item, such as product variants. +- [Inventory Across Locations](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventorylevel/index.html.md): Manage inventory levels across different locations, such as warehouses. +- [Reservation Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#reservationitem/index.html.md): Reserve quantities of inventory items at specific locations for orders or other purposes. +- [Check Inventory Availability](https://docs.medusajs.com/references/inventory-next/confirmInventory/index.html.md): Check whether an inventory item has the necessary quantity for purchase. +- [Inventory Kits](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## How to Use Stock Location Module's Service +## How to Use the Inventory 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. @@ -15798,7 +14601,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-stock-location.ts" highlights={highlights} +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -15807,33 +14610,37 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createInventoryItemStep = createStep( + "create-inventory-item", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", + const inventoryItem = await inventoryModuleService.createInventoryItems({ + sku: "SHIRT", + title: "Green Medusa Shirt", + requires_shipping: true, }) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ inventoryItem }, inventoryItem.id) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (inventoryItemId, { container }) => { + if (!inventoryItemId) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const inventoryModuleService = container.resolve(Modules.INVENTORY) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await inventoryModuleService.deleteInventoryItems([inventoryItemId]) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", () => { - const { stockLocation } = createStockLocationStep() + const { inventoryItem } = createInventoryItemStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ + inventoryItem, + }) } ) ``` @@ -15847,13 +14654,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createInventoryItemWorkflow(req.scope) .run() res.send(result) @@ -15867,13 +14674,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15888,12 +14695,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createInventoryItemWorkflow(container) .run() console.log(result) @@ -15910,36 +14717,22 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Sales Channel Module +# Currency Module -In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. +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. -Medusa has sales channel related features available out-of-the-box through the Sales Channel 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 Sales Channel Module. +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). -## What's a Sales Channel? - -A sales channel indicates an online or offline channel that you sell products on. - -Some use case examples for using a sales channel: - -- Implement a B2B Ecommerce Store. -- Specify different products for each channel you sell in. -- Support omnichannel in your ecommerce store. - -*** - -## Sales Channel Features +## Currency Features -- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. -- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. -- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. -- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. +- [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 Sales Channel Module's Service +## 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. @@ -15947,50 +14740,46 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} +```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 createSalesChannelStep = createStep( - "create-sales-channel", +const retrieveCurrencyStep = createStep( + "retrieve-currency", async ({}, { container }) => { - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) - - const salesChannels = await salesChannelModuleService.createSalesChannels([ - { - name: "B2B", - }, - { - name: "Mobile App", - }, - ]) + const currencyModuleService = container.resolve(Modules.CURRENCY) - return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) - }, - async (salesChannelIds, { container }) => { - if (!salesChannelIds) { - return - } - const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) + const currency = await currencyModuleService + .retrieveCurrency("usd") - await salesChannelModuleService.deleteSalesChannels( - salesChannelIds - ) + return new StepResponse({ currency }) } ) -export const createSalesChannelWorkflow = createWorkflow( - "create-sales-channel", - () => { - const { salesChannels } = createSalesChannelStep() +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({ - salesChannels, + formattedPrice, }) } ) @@ -16000,19 +14789,21 @@ You can then execute the workflow in your custom API routes, scheduled jobs, or ### API Route -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```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 { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createSalesChannelWorkflow(req.scope) - .run() + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) res.send(result) } @@ -16020,19 +14811,21 @@ export async function GET( ### Subscriber -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +```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 { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createSalesChannelWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -16044,15 +14837,17 @@ export const config: SubscriberConfig = { ### Scheduled Job -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createSalesChannelWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -16068,24 +14863,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Promotion Module +# Fulfillment 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 Fulfillment Module and how to use it in your application. -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 fulfillment related features available out-of-the-box through the Fulfillment 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 Fulfillment 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 +## Fulfillment 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. +- [Fulfillment Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/item-fulfillment/index.html.md): Create fulfillments and keep track of their status, items, and more. +- [Integrate Third-Party Fulfillment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/fulfillment-provider/index.html.md): Create third-party fulfillment providers to provide customers with shipping options and fulfill their orders. +- [Restrict By Location and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/shipping-option/index.html.md): Shipping options can be restricted to specific geographical locations. You can also specify custom rules to restrict shipping options. +- [Support Different Fulfillment Forms](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts/index.html.md): Support various fulfillment forms, such as shipping or pick up. *** -## How to Use the Promotion Module +## How to Use the Fulfillment 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. @@ -16093,7 +14888,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-fulfillment.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16102,41 +14897,50 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPromotionStep = createStep( - "create-promotion", +const createFulfillmentStep = createStep( + "create-fulfillment", async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, - currency_code: "usd", + const fulfillment = await fulfillmentModuleService.createFulfillment({ + location_id: "loc_123", + provider_id: "webshipper", + delivery_address: { + country_code: "us", + city: "Strongsville", + address_1: "18290 Royalton Rd", }, + items: [ + { + title: "Shirt", + sku: "SHIRT", + quantity: 1, + barcode: "123456", + }, + ], + labels: [], + order: {}, }) - return new StepResponse({ promotion }, promotion.id) + return new StepResponse({ fulfillment }, fulfillment.id) }, - async (promotionId, { container }) => { - if (!promotionId) { + async (fulfillmentId, { container }) => { + if (!fulfillmentId) { return } - const promotionModuleService = container.resolve(Modules.PROMOTION) + const fulfillmentModuleService = container.resolve(Modules.FULFILLMENT) - await promotionModuleService.deletePromotions(promotionId) + await fulfillmentModuleService.deleteFulfillment(fulfillmentId) } ) -export const createPromotionWorkflow = createWorkflow( - "create-promotion", +export const createFulfillmentWorkflow = createWorkflow( + "create-fulfillment", () => { - const { promotion } = createPromotionStep() + const { fulfillment } = createFulfillmentStep() return new WorkflowResponse({ - promotion, + fulfillment, }) } ) @@ -16151,13 +14955,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" +import { createFulfillmentWorkflow } from "../../workflows/create-fuilfillment" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPromotionWorkflow(req.scope) + const { result } = await createFulfillmentWorkflow(req.scope) .run() res.send(result) @@ -16171,13 +14975,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createFulfillmentWorkflow(container) .run() console.log(result) @@ -16192,12 +14996,12 @@ 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 { createFulfillmentWorkflow } from "../workflows/create-fuilfillment" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createFulfillmentWorkflow(container) .run() console.log(result) @@ -16213,23 +15017,32 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Fulfillment Module -# Store Module +The Fulfillment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) for details on the module's options. -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. +*** -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). +# Order Module -## Store Features +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. -- [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. +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 Store Module's Service +## 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. @@ -16237,7 +15050,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-store.ts" highlights={highlights} +```ts title="src/workflows/create-draft-order.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16246,37 +15059,49 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStoreStep = createStep( - "create-store", +const createDraftOrderStep = createStep( + "create-order", async ({}, { container }) => { - const storeModuleService = container.resolve(Modules.STORE) + const orderModuleService = container.resolve(Modules.ORDER) - const store = await storeModuleService.createStores({ - name: "My Store", - supported_currencies: [{ - currency_code: "usd", - is_default: true, - }], + 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({ store }, store.id) + return new StepResponse({ draftOrder }, draftOrder.id) }, - async (storeId, { container }) => { - if(!storeId) { + async (draftOrderId, { container }) => { + if (!draftOrderId) { return } - const storeModuleService = container.resolve(Modules.STORE) - - await storeModuleService.deleteStores([storeId]) + const orderModuleService = container.resolve(Modules.ORDER) + + await orderModuleService.deleteOrders([draftOrderId]) } ) -export const createStoreWorkflow = createWorkflow( - "create-store", +export const createDraftOrderWorkflow = createWorkflow( + "create-draft-order", () => { - const { store } = createStoreStep() + const { draftOrder } = createDraftOrderStep() - return new WorkflowResponse({ store }) + return new WorkflowResponse({ + draftOrder, + }) } ) ``` @@ -16290,13 +15115,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStoreWorkflow } from "../../workflows/create-store" +import { createDraftOrderWorkflow } from "../../workflows/create-draft-order" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStoreWorkflow(req.scope) + const { result } = await createDraftOrderWorkflow(req.scope) .run() res.send(result) @@ -16310,13 +15135,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStoreWorkflow } from "../workflows/create-store" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStoreWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -16331,12 +15156,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createStoreWorkflow } from "../workflows/create-store" +import { createDraftOrderWorkflow } from "../workflows/create-draft-order" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStoreWorkflow(container) + const { result } = await createDraftOrderWorkflow(container) .run() console.log(result) @@ -16353,23 +15178,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Tax Module +# Payment 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. +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. -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. +Medusa has payment related features available out-of-the-box through the Payment 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 Payment 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 +## Payment 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. +- [Authorize, Capture, and Refund Payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md): Authorize, capture, and refund payments for a single resource. +- [Payment Collection Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection/index.html.md): Store and manage all payments of a single resources, such as a cart, in payment collections. +- [Integrate Third-Party Payment Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md): Use payment providers like [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to handle and process payments, or integrate custom payment providers. +- [Handle Webhook Events](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/webhook-events/index.html.md): Handle webhook events from third-party providers and process the associated payment. *** -## How to Use Tax Module's Service +## How to Use the Payment 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. @@ -16377,7 +15203,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} +```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16386,33 +15212,36 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createTaxRegionStep = createStep( - "create-tax-region", +const createPaymentCollectionStep = createStep( + "create-payment-collection", async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) + const paymentModuleService = container.resolve(Modules.PAYMENT) - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, }) - return new StepResponse({ taxRegion }, taxRegion.id) + return new StepResponse({ paymentCollection }, paymentCollection.id) }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { return } - const taxModuleService = container.resolve(Modules.TAX) + const paymentModuleService = container.resolve(Modules.PAYMENT) - await taxModuleService.deleteTaxRegions([taxRegionId]) + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) } ) -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", () => { - const { taxRegion } = createTaxRegionStep() + const { paymentCollection } = createPaymentCollectionStep() - return new WorkflowResponse({ taxRegion }) + return new WorkflowResponse({ + paymentCollection, + }) } ) ``` @@ -16426,13 +15255,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" +import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createTaxRegionWorkflow(req.scope) + const { result } = await createPaymentCollectionWorkflow(req.scope) .run() res.send(result) @@ -16446,13 +15275,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -16467,12 +15296,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createTaxRegionWorkflow(container) + const { result } = await createPaymentCollectionWorkflow(container) .run() console.log(result) @@ -16488,29 +15317,38 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure Tax Module +## Configure Payment 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. +The Payment Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) for details on the module's options. *** +## Providers -# User Module +Medusa provides the following payment providers out-of-the-box. You can use them to process payments for orders, returns, and other resources. -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. +*** -Medusa has user related features available out-of-the-box through the User 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 User Module. + +# 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. + +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). -## User Features +## Pricing Features -- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. -- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. +- [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 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 User Module's Service +## 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. @@ -16518,7 +15356,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-user.ts" highlights={highlights} +```ts title="src/workflows/create-price-set.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -16527,36 +15365,46 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createUserStep = createStep( - "create-user", +const createPriceSetStep = createStep( + "create-price-set", async ({}, { container }) => { - const userModuleService = container.resolve(Modules.USER) + const pricingModuleService = container.resolve(Modules.PRICING) - const user = await userModuleService.createUsers({ - email: "user@example.com", - first_name: "John", - last_name: "Smith", + 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({ user }, user.id) + return new StepResponse({ priceSet }, priceSet.id) }, - async (userId, { container }) => { - if (!userId) { + async (priceSetId, { container }) => { + if (!priceSetId) { return } - const userModuleService = container.resolve(Modules.USER) + const pricingModuleService = container.resolve(Modules.PRICING) - await userModuleService.deleteUsers([userId]) + await pricingModuleService.deletePriceSets([priceSetId]) } ) -export const createUserWorkflow = createWorkflow( - "create-user", +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", () => { - const { user } = createUserStep() + const { priceSet } = createPriceSetStep() return new WorkflowResponse({ - user, + priceSet, }) } ) @@ -16571,13 +15419,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createUserWorkflow } from "../../workflows/create-user" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createUserWorkflow(req.scope) + const { result } = await createPriceSetWorkflow(req.scope) .run() res.send(result) @@ -16591,13 +15439,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createUserWorkflow } from "../workflows/create-user" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createUserWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -16612,12 +15460,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createUserWorkflow } from "../workflows/create-user" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createUserWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -16633,1041 +15481,1294 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -## Configure User Module - -The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. - -*** - - -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. -## API Key Types +# Region Module -There are two types of API keys: +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. -- `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. +Medusa has region related features available out-of-the-box through the Region 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 Region Module. -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). +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/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). +## Region Features -The associated token is no longer usable or verifiable. +- [Region Management](https://docs.medusajs.com/references/region/models/Region/index.html.md): Manage regions in your store. You can create regions with different currencies and settings. +- [Multi-Currency Support](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has a currency. You can support multiple currencies in your store by creating multiple regions. +- [Different Settings Per Region](https://docs.medusajs.com/references/region/models/Region/index.html.md): Each region has its own settings, such as what countries belong to a region or its tax settings. *** -## Token Verification +## How to Use Region Module's Service -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. +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. -# Links between API Key Module and Other Modules - -This document showcases the module links defined between the API Key Module and other commerce modules. - -## Summary - -The API Key Module has the following links to other modules: +For example: -- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). +```ts title="src/workflows/create-region.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -*** +const createRegionStep = createStep( + "create-region", + async ({}, { container }) => { + const regionModuleService = container.resolve(Modules.REGION) -## Sales Channel Module + const region = await regionModuleService.createRegions({ + name: "Europe", + currency_code: "eur", + }) -You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. + return new StepResponse({ region }, region.id) + }, + async (regionId, { container }) => { + if (!regionId) { + return + } + const regionModuleService = container.resolve(Modules.REGION) -![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) + await regionModuleService.deleteRegions([regionId]) + } +) -This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. +export const createRegionWorkflow = createWorkflow( + "create-region", + () => { + const { region } = createRegionStep() -Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). + return new WorkflowResponse({ + region, + }) + } +) +``` -### Retrieve with Query +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: +### API Route -### query.graph +```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 { createRegionWorkflow } from "../../workflows/create-region" -```ts -const { data: apiKeys } = await query.graph({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createRegionWorkflow(req.scope) + .run() -// apiKeys.sales_channels + res.send(result) +} ``` -### useQueryGraphStep +### Subscriber -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```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 { createRegionWorkflow } from "../workflows/create-region" -// ... +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createRegionWorkflow(container) + .run() -const { data: apiKeys } = useQueryGraphStep({ - entity: "api_key", - fields: [ - "sales_channels.*", - ], -}) + console.log(result) +} -// apiKeys.sales_channels +export const config: SubscriberConfig = { + event: "user.created", +} ``` -### Manage with Link - -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +### Scheduled Job -### link.create +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createRegionWorkflow } from "../workflows/create-region" -```ts -import { Modules } from "@medusajs/framework/utils" +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createRegionWorkflow(container) + .run() -// ... + console.log(result) +} -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} ``` -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -// ... +*** -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +# Promotion Module -# Cart Concepts +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 document, you’ll get an overview of the main concepts of a cart. +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. -## Shipping and Billing Addresses +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). +## Promotion Features -![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) +- [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. *** -## Line Items - -A line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. +## How to Use the Promotion Module -A line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. +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. -In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/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: -## Shipping Methods +```ts title="src/workflows/create-promotion.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -A shipping method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. +const createPromotionStep = createStep( + "create-promotion", + async ({}, { container }) => { + const promotionModuleService = container.resolve(Modules.PROMOTION) -In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, + currency_code: "usd", + }, + }) -### data Property + return new StepResponse({ promotion }, promotion.id) + }, + async (promotionId, { container }) => { + if (!promotionId) { + return + } + const promotionModuleService = container.resolve(Modules.PROMOTION) -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. + await promotionModuleService.deletePromotions(promotionId) + } +) -If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. +export const createPromotionWorkflow = createWorkflow( + "create-promotion", + () => { + const { promotion } = createPromotionStep() -The `data` property is an object used to store custom data relevant later for fulfillment. + return new WorkflowResponse({ + promotion, + }) + } +) +``` +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -# Promotions Adjustments in Carts +### API Route -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. +```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 { createPromotionWorkflow } from "../../workflows/create-cart" -## What are Adjustment Lines? +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPromotionWorkflow(req.scope) + .run() -An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. + res.send(result) +} +``` -The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. +### Subscriber -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) +```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 { createPromotionWorkflow } from "../workflows/create-cart" -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPromotionWorkflow(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 { createPromotionWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPromotionWorkflow(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). *** -## Discountable Option -The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. +# Product Module -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. +In this section of the documentation, you will find resources to learn more about the Product Module and how to use it in your application. + +Medusa has product related features available out-of-the-box through the Product 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 Product Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Product Features + +- [Products Management](https://docs.medusajs.com/references/product/models/Product/index.html.md): Store and manage products. Products have custom options, such as color or size, and each variant in the product sets the value for these options. +- [Product Organization](https://docs.medusajs.com/references/product/models/index.html.md): The Product Module provides different data models used to organize products, including categories, collections, tags, and more. +- [Bundled and Multi-Part Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Create and manage inventory kits for a single product, allowing you to implement use cases like bundled or multi-part products. *** -## Promotion Actions +## How to Use the Product Module -When using the Cart and Promotion modules together, such as in the Medusa application, 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. +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. -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). +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 collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" +```ts title="src/workflows/create-product.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) +const createProductStep = createStep( + "create-product", + async ({}, { container }) => { + const productService = container.resolve(Modules.PRODUCT) -// retrieve line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -cart.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - adjustments: filteredAdjustments, + const product = await productService.createProducts({ + title: "Medusa Shirt", + options: [ + { + title: "Color", + values: ["Black", "White"], + }, + ], + variants: [ + { + title: "Black Shirt", + options: { + Color: "Black", + }, + }, + ], }) - } -}) -// retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -cart.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, - }) + return new StepResponse({ product }, product.id) + }, + async (productId, { container }) => { + if (!productId) { + return + } + const productService = container.resolve(Modules.PRODUCT) + + await productService.deleteProducts([productId]) } -}) +) -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, +export const createProductWorkflow = createWorkflow( + "create-product", + () => { + const { product } = createProductStep() + + return new WorkflowResponse({ + product, + }) } ) ``` -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. +### API Route -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +```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 { createProductWorkflow } from "../../workflows/create-product" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createProductWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createProductWorkflow } from "../workflows/create-product" -// ... +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createProductWorkflow(container) + .run() -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) + console.log(result) +} -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) +export const config: SubscriberConfig = { + event: "user.created", +} ``` +### Scheduled Job -# Links between Cart Module and Other Modules - -This document showcases the module links defined between the Cart Module and other commerce modules. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createProductWorkflow } from "../workflows/create-product" -## Summary +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createProductWorkflow(container) + .run() -The Cart Module has the following links to other modules: + console.log(result) +} -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -- [`Cart` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model of Order Module \<> `Cart` data model](#order-module). -- [`Cart` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`LineItem` data model \<> `Product` data model of Product Module](#product-module). (Read-only). -- [`LineItem` data model \<> `ProductVariant` data model of Product Module](#product-module). (Read-only). -- [`Cart` data model \<> `Promotion` data model of Promotion Module](#promotion-module). -- [`Cart` data model \<> `Region` data model of Region Module](#region-module). (Read-only). -- [`Cart` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Customer Module -Medusa defines a read-only link between the `Cart` 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 a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +# Sales Channel Module -### Retrieve with Query +In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. -To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: +Medusa has sales channel related features available out-of-the-box through the Sales Channel 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 Sales Channel Module. -### query.graph +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +## What's a Sales Channel? -// carts.order -``` +A sales channel indicates an online or offline channel that you sell products on. -### useQueryGraphStep +Some use case examples for using a sales channel: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +- Implement a B2B Ecommerce Store. +- Specify different products for each channel you sell in. +- Support omnichannel in your ecommerce store. -// ... +*** -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) +## Sales Channel Features -// carts.order -``` +- [Sales Channel Management](https://docs.medusajs.com/references/sales-channel/models/SalesChannel/index.html.md): Manage sales channels in your store. Each sales channel has different meta information such as name or description, allowing you to easily differentiate between sales channels. +- [Product Availability](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa uses the Product and Sales Channel modules to allow merchants to specify a product's availability per sales channel. +- [Cart and Order Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Carts, available through the Cart Module, are scoped to a sales channel. Paired with the product availability feature, you benefit from more features like allowing only products available in sales channel in a cart. +- [Inventory Availability Per Sales Channel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/links-to-other-modules/index.html.md): Medusa links sales channels to stock locations, allowing you to retrieve available inventory of products based on the specified sales channel. *** -## Order Module +## How to Use Sales Channel Module's Service -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. +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. -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. +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. -![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) +For example: -### Retrieve with Query +```ts title="src/workflows/create-sales-channel.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: +const createSalesChannelStep = createStep( + "create-sales-channel", + async ({}, { container }) => { + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) -### query.graph + const salesChannels = await salesChannelModuleService.createSalesChannels([ + { + name: "B2B", + }, + { + name: "Mobile App", + }, + ]) -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) + return new StepResponse({ salesChannels }, salesChannels.map((sc) => sc.id)) + }, + async (salesChannelIds, { container }) => { + if (!salesChannelIds) { + return + } + const salesChannelModuleService = container.resolve(Modules.SALES_CHANNEL) -// carts.order + await salesChannelModuleService.deleteSalesChannels( + salesChannelIds + ) + } +) + +export const createSalesChannelWorkflow = createWorkflow( + "create-sales-channel", + () => { + const { salesChannels } = createSalesChannelStep() + + return new WorkflowResponse({ + salesChannels, + }) + } +) ``` -### useQueryGraphStep +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### 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 { createSalesChannelWorkflow } from "../../workflows/create-sales-channel" -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createSalesChannelWorkflow(req.scope) + .run() -// carts.order + res.send(result) +} ``` -### 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): +### Subscriber -### link.create +```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 { createSalesChannelWorkflow } from "../workflows/create-sales-channel" -```ts -import { Modules } from "@medusajs/framework/utils" +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createSalesChannelWorkflow(container) + .run() -// ... + console.log(result) +} -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) +export const config: SubscriberConfig = { + event: "user.created", +} ``` -### createRemoteLinkStep +### Scheduled Job -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createSalesChannelWorkflow } from "../workflows/create-sales-channel" -// ... +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createSalesChannelWorkflow(container) + .run() -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) + 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). -## Payment Module +*** -The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. +# Stock Location Module -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) +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. -### Retrieve with Query +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. -To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -### query.graph +## Stock Location Features -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +- [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. -// carts.payment_collection -``` +*** -### useQueryGraphStep +## How to Use Stock Location Module's Service -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) +For example: -// carts.payment_collection -``` +```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" -### Manage with Link +const createStockLocationStep = createStep( + "create-stock-location", + async ({}, { container }) => { + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) -To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", + }) -### link.create + return new StepResponse({ stockLocation }, stockLocation.id) + }, + async (stockLocationId, { container }) => { + if (!stockLocationId) { + return + } + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) -```ts -import { Modules } from "@medusajs/framework/utils" + await stockLocationModuleService.deleteStockLocations([stockLocationId]) + } +) -// ... +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", + () => { + const { stockLocation } = createStockLocationStep() -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) + return new WorkflowResponse({ stockLocation }) + } +) ``` -### createRemoteLinkStep +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +### 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" -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStockLocationWorkflow(req.scope) + .run() + + res.send(result) +} ``` -*** +### Subscriber -## Product Module +```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" -Medusa defines read-only links between: +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStockLocationWorkflow(container) + .run() -- the `LineItem` 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 `LineItem` data model. -- the `LineItem` 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 `LineItem` data model. + console.log(result) +} -### Retrieve with Query +export const config: SubscriberConfig = { + event: "user.created", +} +``` -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`: +### Scheduled Job -To retrieve the product, pass `product.*` in `fields`. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" -### query.graph +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStockLocationWorkflow(container) + .run() -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) + console.log(result) +} -// lineItems.variant +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) +*** -// lineItems.variant -``` -*** +# Store Module -## Promotion 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. -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. +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. -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -![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) +## Store Features -Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. +- [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. -### Retrieve with Query +*** -To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: +## How to Use Store Module's Service -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. +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. -### query.graph +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. -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) +For example: -// carts.promotions -``` +```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" -### useQueryGraphStep +const createStoreStep = createStep( + "create-store", + async ({}, { container }) => { + const storeModuleService = container.resolve(Modules.STORE) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + 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]) + } +) -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) +export const createStoreWorkflow = createWorkflow( + "create-store", + () => { + const { store } = createStoreStep() -// carts.promotions + return new WorkflowResponse({ store }) + } +) ``` -### 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): +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -### link.create +### API Route -```ts -import { Modules } from "@medusajs/framework/utils" +```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() -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) + res.send(result) +} ``` -### createRemoteLinkStep +### Subscriber -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +```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() -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} ``` -*** +### Scheduled Job -## Region Module +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStoreWorkflow } from "../workflows/create-store" -Medusa defines a read-only link between the `Cart` 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 a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStoreWorkflow(container) + .run() -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) + console.log(result) +} -// carts.region +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} ``` -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts.region -``` +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Sales Channel Module - -Medusa defines a read-only link between the `Cart` 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 a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the sales channel of a cart 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: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) -// carts.sales_channel -``` +# Tax Module -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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). -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) +## Tax Features -// carts.sales_channel -``` +- [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. +*** -# Tax Lines in Cart Module +## How to Use Tax Module's Service -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax 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. -## What are Tax Lines? +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. -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. +For example: -![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) +```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) -## Tax Inclusivity + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", + }) -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. + return new StepResponse({ taxRegion }, taxRegion.id) + }, + async (taxRegionId, { container }) => { + if (!taxRegionId) { + return + } + const taxModuleService = container.resolve(Modules.TAX) -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. + await taxModuleService.deleteTaxRegions([taxRegionId]) + } +) -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", + () => { + const { taxRegion } = createTaxRegionStep() -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. + return new WorkflowResponse({ taxRegion }) + } +) +``` -![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) +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -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`. +### 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" -## Retrieve Tax Lines +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createTaxRegionWorkflow(req.scope) + .run() -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. + res.send(result) +} +``` -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) +### Subscriber -// 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", - }, - } -) -``` +```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" -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createTaxRegionWorkflow(container) + .run() -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) + console.log(result) +} -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) +export const config: SubscriberConfig = { + event: "user.created", +} ``` +### Scheduled Job -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" -## `has_account` Property +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createTaxRegionWorkflow(container) + .run() -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. + console.log(result) +} -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -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`. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Email Uniqueness +## Configure Tax Module -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. +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. -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 +# User Module -This document showcases the module links defined between the Customer Module and other commerce modules. +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. -## Summary +Medusa has user related features available out-of-the-box through the User 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 User Module. -The Customer Module has the following links to other modules: +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +## User Features -- [`Cart` data model of Cart Module \<> `Customer` data model](#cart-module). (Read-only). -- [`Order` data model of Order Module \<> `Customer` data model](#order-module). (Read-only). +- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. +- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. *** -## Cart Module +## How to Use User Module's Service -Medusa defines a read-only link between the `Customer` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a customer's carts, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. +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. -### Retrieve with Query +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. -To retrieve a customer's carts with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: - -### query.graph +For example: -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "carts.*", - ], -}) +```ts title="src/workflows/create-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" -// customers.carts -``` +const createUserStep = createStep( + "create-user", + async ({}, { container }) => { + const userModuleService = container.resolve(Modules.USER) -### useQueryGraphStep + const user = await userModuleService.createUsers({ + email: "user@example.com", + first_name: "John", + last_name: "Smith", + }) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + return new StepResponse({ user }, user.id) + }, + async (userId, { container }) => { + if (!userId) { + return + } + const userModuleService = container.resolve(Modules.USER) -// ... + await userModuleService.deleteUsers([userId]) + } +) -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "carts.*", - ], -}) +export const createUserWorkflow = createWorkflow( + "create-user", + () => { + const { user } = createUserStep() -// customers.carts + return new WorkflowResponse({ + user, + }) + } +) ``` -*** - -## Order Module - -Medusa defines a read-only link between the `Customer` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a customer's orders, 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 +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: -To retrieve a customer's orders with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +### API Route -### query.graph +```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 { createUserWorkflow } from "../../workflows/create-user" -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "orders.*", - ], -}) +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createUserWorkflow(req.scope) + .run() -// customers.orders + res.send(result) +} ``` -### useQueryGraphStep +### Subscriber -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +```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 { createUserWorkflow } from "../workflows/create-user" -// ... +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createUserWorkflow(container) + .run() -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "orders.*", - ], -}) + console.log(result) +} -// customers.orders +export const config: SubscriberConfig = { + event: "user.created", +} ``` +### Scheduled Job -# Auth Identity and Actor Types +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createUserWorkflow } from "../workflows/create-user" -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createUserWorkflow(container) + .run() -## What is an Auth Identity? + console.log(result) +} -The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Actor Types - -An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). - -Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. - -For example, an auth identity of a customer has the following `app_metadata` property: - -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` +## Configure User Module -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. +The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. *** -## Protect Routes by Actor Type -When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. +# API Key Concepts -For example: +In this document, you’ll learn about the different types of API keys, their expiration and verification. -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" +## API Key Types -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], - }, - ], -}) -``` +There are two types of API keys: -By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. +- `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). *** -## Custom Actor Types +## API Key Expiration -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. +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). -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. +The associated token is no longer usable or verifiable. -Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). +*** +## Token Verification -# Auth Providers +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. -In this document, you’ll learn how the Auth Module handles authentication using providers. -## What's an Auth Module Provider? +# Links between API Key Module and Other Modules -An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. +This document showcases the module links defined between the API Key Module and other commerce modules. -For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. +## Summary -### Auth Providers List +The API Key Module has the following links to other modules: -- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) -- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) -- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) +- [`ApiKey` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). *** -## Configure Allowed Auth Providers of Actor Types +## Sales Channel Module -By default, users of all actor types can authenticate with all installed auth module providers. +You can create a publishable API key and associate it with a sales channel. Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: +![A diagram showcasing an example of how data models from the API Key and Sales Channel modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - authMethodsPerActor: { - user: ["google"], - customer: ["emailpass"], - }, - // ... - }, - // ... - }, -}) -``` +This is useful to avoid passing the sales channel's ID as a parameter of every request, and instead pass the publishable API key in the header of any request to the Store API route. -When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. +Learn more about this in the [Sales Channel Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md). -*** +### Retrieve with Query -## How to Create an Auth Module Provider +To retrieve the sales channels of an API key with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: -Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. +### query.graph +```ts +const { data: apiKeys } = await query.graph({ + entity: "api_key", + fields: [ + "sales_channels.*", + ], +}) -# Authentication Flows with the Auth Main Service +// apiKeys.sales_channels +``` -In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. +### useQueryGraphStep -## Authentication Methods +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: apiKeys } = useQueryGraphStep({ + entity: "api_key", + fields: [ + "sales_channels.*", + ], +}) + +// apiKeys.sales_channels +``` + +### Manage with Link + +To manage the sales channels of an API key, 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.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` + + +# Authentication Flows with the Auth Main Service + +In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. + +## Authentication Methods ### Register @@ -17865,6 +16966,75 @@ In the example above, you use the `emailpass` provider, so you have to pass an o If the returned `success` property is `true`, the password has reset successfully. +# Auth Identity and Actor Types + +In this document, you’ll learn about concepts related to identity and actors in the Auth Module. + +## What is an Auth Identity? + +The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. + +Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. + +*** + +## Actor Types + +An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). + +Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. + +For example, an auth identity of a customer has the following `app_metadata` property: + +```json +{ + "app_metadata": { + "customer_id": "cus_123" + } +} +``` + +The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. + +*** + +## Protect Routes by Actor Type + +When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. + +For example: + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/custom/admin*", + middlewares: [ + authenticate("user", ["session", "bearer", "api-key"]), + ], + }, + ], +}) +``` + +By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. + +*** + +## Custom Actor Types + +You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. + +For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. + +Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). + + # How to Use Authentication Routes In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. @@ -17885,8 +17055,8 @@ The steps are: 1. Register the user with the [Register Route](#register-route). 2. Use the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers/index.html.md). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept/index.html.md) + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) 3. Authenticate the user with the [Auth Route](#login-route). After registration, you only use the [Auth Route](#login-route) for subsequent authentication. @@ -17912,8 +17082,8 @@ It requires the following steps: - If the decoded data has an `actor_id` property, then the user is already registered. So, use this token for subsequent authenticated requests. - If not, follow the rest of the steps. 7. The frontend uses the authentication token to create the user with their respective API route. - - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers/index.html.md). - - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept/index.html.md) + - For example, for customers you would use the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). + - For admin users, you accept an invite using the [Accept Invite API route](https://docs.medusajs.com/api/admin#invites_postinvitesaccept) 8. The frontend sends a request to the [Refresh Token Route](#refresh-token-route) to retrieve a new token with the user information populated. *** @@ -17936,7 +17106,7 @@ This API route is useful for providers like `emailpass` that uses custom logic t For example, if you're registering a customer, you: 1. Send a request to `/auth/customer/emailpass/register` to retrieve the registration JWT token. -2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers/index.html.md) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication/index.html.md). +2. Send a request to the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). ### Path Parameters @@ -17996,7 +17166,7 @@ You can show that error message to the customer. ## Login Route -The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication/index.html.md) to send authenticated requests. +The Medusa application defines an API route at `/auth/{actor_type}/{provider}` that authenticates a user of an actor type. It returns a JWT token that can be passed in [the header of subsequent requests](https://docs.medusajs.com/api/store#authentication) to send authenticated requests. ```bash curl -X POST http://localhost:9000/auth/{actor_type}/{providers} @@ -18088,7 +17258,7 @@ If the authentication is successful, you'll receive a `token` field in the respo In your frontend, decode the token using tools like [react-jwt](https://www.npmjs.com/package/react-jwt): - If the decoded data has an `actor_id` property, the user is already registered. So, use this token for subsequent authenticated requests. -- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers/index.html.md). +- If not, use the token in the header of a request that creates the user, such as the [Create Customer API route](https://docs.medusajs.com/api/store#customers_postcustomers). *** @@ -18206,6 +17376,54 @@ If the authentication is successful, the request returns an object with a `succe ``` +# Auth Providers + +In this document, you’ll learn how the Auth Module handles authentication using providers. + +## What's an Auth Module Provider? + +An auth module provider handles authenticating customers and users, either using custom logic or by integrating a third-party service. + +For example, the EmailPass Auth Module Provider authenticates a user using their email and password, whereas the Google Auth Module Provider authenticates users using their Google account. + +### Auth Providers List + +- [Emailpass](https://docs.medusajs.com/commerce-modules/auth/auth-providers/emailpass/index.html.md) +- [Google](https://docs.medusajs.com/commerce-modules/auth/auth-providers/google/index.html.md) +- [GitHub](https://docs.medusajs.com/commerce-modules/auth/auth-providers/github/index.html.md) + +*** + +## Configure Allowed Auth Providers of Actor Types + +By default, users of all actor types can authenticate with all installed auth module providers. + +To restrict the auth providers used for actor types, use the [authMethodsPerActor option](https://docs.medusajs.com/references/medusa-config#http-authMethodsPerActor-1-3/index.html.md) in Medusa's configurations: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + authMethodsPerActor: { + user: ["google"], + customer: ["emailpass"], + }, + // ... + }, + // ... + }, +}) +``` + +When you specify the `authMethodsPerActor` configuration, it overrides the default. So, if you don't specify any providers for an actor type, users of that actor type can't authenticate with any provider. + +*** + +## How to Create an Auth Module Provider + +Refer to [this guide](https://docs.medusajs.com/references/auth/provider/index.html.md) to learn how to create an auth module provider. + + # How to Create an Actor Type In this document, learn how to create an actor type and authenticate its associated data model. @@ -18777,87 +17995,83 @@ 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 - -In this document, you’ll learn about some basic fulfillment concepts. +# Cart Concepts -## Fulfillment Set +In this document, you’ll get an overview of the main concepts of a cart. -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. +## Shipping and Billing Addresses -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. +A cart has a shipping and billing address. Both of these addresses are represented by the [Address data model](https://docs.medusajs.com/references/cart/models/Address/index.html.md). -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` +![A diagram showcasing the relation between the Cart and Address data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711532392/Medusa%20Resources/cart-addresses_ls6qmv.jpg) *** -## 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. +## Line Items -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 line item, represented by the [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model, is a quantity of a product variant added to the cart. A cart has multiple line items. -![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 line item stores some of the product variant’s properties, such as the `product_title` and `product_description`. It also stores data related to the item’s quantity and price. -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. +In the Medusa application, a product variant is implemented in the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md). *** -## Shipping Profile +## Shipping Methods -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 method, represented by the [ShippingMethod data model](https://docs.medusajs.com/references/cart/models/ShippingMethod/index.html.md), is used to fulfill the items in the cart after the order is placed. A cart can have more than one shipping method. -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. +In the Medusa application, the shipping method is created from a shipping option, available through the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md). Its ID is stored in the `shipping_option_id` property of the method. +### data Property -# Links between Currency Module and Other Modules +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. -This document showcases the module links defined between the Currency Module and other commerce modules. +If the fulfillment provider requires additional custom data to be passed along from the checkout process, set this data in the `ShippingMethod`'s `data` property. + +The `data` property is an object used to store custom data relevant later for fulfillment. + + +# Links between Cart Module and Other Modules + +This document showcases the module links defined between the Cart Module and other commerce modules. ## Summary -The Currency Module has the following links to other modules: +The Cart 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. -- [`Currency` data model of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). +- [`Cart` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). +- [`Order` data model of Order Module \<> `Cart` data model](#order-module). +- [`Cart` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). +- [`LineItem` data model \<> `Product` data model of Product Module](#product-module). (Read-only). +- [`LineItem` data model \<> `ProductVariant` data model of Product Module](#product-module). (Read-only). +- [`Cart` data model \<> `Promotion` data model of Promotion Module](#promotion-module). +- [`Cart` data model \<> `Region` data model of Region Module](#region-module). (Read-only). +- [`Cart` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). *** -## 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. +## Customer Module -Instead, Medusa defines a read-only link between the Currency Module's `Currency` data model and the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` 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` data model in the Store Module. +Medusa defines a read-only link between the `Cart` 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 a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. ### 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`: +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "customer.*", ], }) -// stores.supported_currencies +// carts.order ``` ### useQueryGraphStep @@ -18867,140 +18081,41 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "customer.*", ], }) -// stores.supported_currencies +// carts.order ``` - -# 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. - - -# Fulfillment Module Provider - -In this document, you’ll learn what a fulfillment module provider is. - -## 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: - -- [`Order` data model of the Order Module \<> `Fulfillment` data model](#order-module). -- [`Return` data model of the Order Module \<> `Fulfillment` data model](#order-module). -- [`PriceSet` data model of the Pricing Module \<> `ShippingOption` data model](#pricing-module). -- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentProvider` data model](#stock-location-module). -- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentSet` data model](#stock-location-module). - *** ## 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) +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. -![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) +![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 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`. +To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: ### query.graph ```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ "order.*", ], }) -// fulfillments.order +// carts.order ``` ### useQueryGraphStep @@ -19010,14 +18125,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ "order.*", ], }) -// fulfillments.order +// carts.order ``` ### Manage with Link @@ -19032,12 +18147,12 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, [Modules.ORDER]: { order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, }) ``` @@ -19050,40 +18165,40 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, [Modules.ORDER]: { order_id: "order_123", }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, }) ``` *** -## Pricing Module +## Payment Module -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. +The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. -![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) +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.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`: +To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: ### query.graph ```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "price_set.*", + "payment_collection.*", ], }) -// shippingOptions.price_set +// carts.payment_collection ``` ### useQueryGraphStep @@ -19093,19 +18208,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "price_set.*", + "payment_collection.*", ], }) -// shippingOptions.price_set +// carts.payment_collection ``` ### 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): +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -19115,11 +18230,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` @@ -19127,52 +18242,46 @@ await link.create({ ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_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) +## Product Module -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +Medusa defines read-only links between: -![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) +- the `LineItem` 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 `LineItem` data model. +- the `LineItem` 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 `LineItem` data model. ### 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 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 stock location of a fulfillment provider, pass `locations.*` in `fields`. +To retrieve the product, pass `product.*` in `fields`. ### query.graph ```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", +const { data: lineItems } = await query.graph({ + entity: "line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets.location +// lineItems.variant ``` ### useQueryGraphStep @@ -19182,644 +18291,593 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", fields: [ - "location.*", + "variant.*", ], }) -// fulfillmentSets.location +// lineItems.variant ``` -### 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): +## Promotion Module -### link.create +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. -```ts -import { Modules } from "@medusajs/framework/utils" +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. -// ... +![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) -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, +Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. + +### Retrieve with Query + +To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], }) + +// carts.promotions ``` -### createRemoteLinkStep +### useQueryGraphStep ```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], }) + +// carts.promotions ``` +### Manage with Link -# Fulfillment Module Options +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you'll learn about the options of the Fulfillment Module. +### link.create -## providers +```ts +import { Modules } from "@medusajs/framework/utils" -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. +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` -For example: +### createRemoteLinkStep -```ts title="medusa-config.ts" +```ts import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, - ], +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, }) ``` -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. - - -# Shipping Option +*** -In this document, you’ll learn about shipping options and their rules. +## Region Module -## What’s a Shipping Option? +Medusa defines a read-only link between the `Cart` 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 a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. -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. +### Retrieve with Query -When the customer places their order, they choose a shipping option to be used to fulfill their items. +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). +### query.graph -*** +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) -## Service Zone Restrictions +// carts.region +``` -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. +### useQueryGraphStep -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -![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. +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) -![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) +// carts.region +``` *** -## Shipping Option Rules - -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. - -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: +## Sales Channel Module -- `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. +Medusa defines a read-only link between the `Cart` 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 a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. -![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) +### Retrieve with Query -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. +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: -![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) +### query.graph -*** +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) -## Shipping Profile and Types +// carts.sales_channel +``` -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). +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -*** +// ... -## data Property +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) -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. +// carts.sales_channel +``` -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. +# Promotions Adjustments in Carts -# Inventory Concepts +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +## What are Adjustment Lines? -## InventoryItem +An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. -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 [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. -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 relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) -![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) +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. *** -## 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. +## Discountable Option -### Associated Location +The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. -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. +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. *** -## ReservationItem +## Promotion Actions -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. +When using the Cart and Promotion modules together, such as in the Medusa application, 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. -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +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). +For example: -# Inventory Module in Medusa Flows +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" -This document explains how the Inventory Module is used within the Medusa application's flows. +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) -## Product Variant Creation +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) + } +}) -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. +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.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, + }) + } +}) -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) +``` -![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) +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 cart’s line item and the shipping method’s adjustments. -## Add to Cart +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" -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) +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) -![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) +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` -*** -## Order Placed +# Tax Lines in Cart Module -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +## What are Tax Lines? -![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) +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) *** -## Order Fulfillment +## Tax Inclusivity -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: +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. -- 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. +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. -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. -![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) +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`. *** -## Order Return +## Retrieve Tax Lines -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. +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. -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) -![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) +// 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", + }, + } +) +``` -### Dismissed Returned Items +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +```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) +) +``` -# 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. +# Customer Accounts -## What is an Inventory Kit? +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. -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. +## `has_account` Property -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. +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. -Using inventory kits, you can implement use cases like: +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. -- [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. +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`. *** -## Multi-Part Products +## Email Uniqueness -Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. +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. -To implement this in Medusa, you can: +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. -- 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. +# Links between Customer Module and Other Modules -![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) +This document showcases the module links defined between the Customer Module and other commerce modules. -### Create Multi-Part Product +## Summary -Using the Medusa Admin, you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). +The Customer Module has the following links to other modules: -Using [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), you can implement this by first creating the inventory items: +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -```ts highlights={multiPartsHighlights1} -import { - createInventoryItemsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" +- [`Cart` data model of Cart Module \<> `Customer` data model](#cart-module). (Read-only). +- [`Order` data model of Order Module \<> `Customer` data model](#order-module). (Read-only). -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, - }, - ], - }, - ], - }, - }) +## Cart Module - // TODO create the product - } -) +Medusa defines a read-only link between the `Customer` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a customer's carts, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve a customer's carts with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "carts.*", + ], +}) + +// customers.carts ``` -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). +### useQueryGraphStep -Then, you create the inventory items that the product variant consists of. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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" +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "carts.*", + ], +}) -export const createMultiPartProductsWorkflow = createWorkflow( - "create-multi-part-products", - () => { - // ... +// customers.carts +``` - 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"], - }, - ], - }, - ], - }, - }) - } -) -``` +## Order Module -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. +Medusa defines a read-only link between the `Customer` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a customer's orders, 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. -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). +### Retrieve with Query -*** +To retrieve a customer's orders with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: -## Bundled Products +### query.graph -Consider you have three products: shirt, pants, and shoes. You sell those products separately, but you also want to offer them as a bundle. +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "orders.*", + ], +}) -![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) +// customers.orders +``` -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. +### useQueryGraphStep -Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -![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 +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "orders.*", + ], +}) -You can create a bundled product in the Medusa Admin 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. +// customers.orders +``` -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" +# Inventory Concepts -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Shirt", - 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", - 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", - variants: [ - { - title: "Shoes", - prices: [ - { - amount: 10, - currency_code: "usd", - }, - ], - options: { - "Default Option": "Default Variant", - }, - manage_inventory: true, - }, - ], - options: [ - { - title: "Default Option", - values: ["Default Variant"], - }, - ], - }, - ], - }, - }) +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. - // TODO re-retrieve with inventory - } -) -``` +## InventoryItem -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). +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. -Next, retrieve the products again but with variant information: +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. -```ts highlights={bundledHighlights2} -import { - // ... - transform, -} from "@medusajs/framework/workflows-sdk" -import { - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" +![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) -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, - }, - }) +## InventoryLevel - const inventoryItemIds = transform({ - productsWithInventory, - }, (data) => { - return data.productsWithInventory.map((product) => { - return { - inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, - } - }) - }) +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. - // create bundled product - } -) -``` +It has three quantity-related properties: -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. +- `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. -Finally, create the bundled product: +### Associated Location -```ts highlights={bundledProductHighlights3} -export const createBundledProducts = createWorkflow( - "create-bundled-products", - () => { - // ... - const bundledProduct = createProductsWorkflow.runAsStep({ - input: { - products: [ - { - title: "Bundled Clothes", - 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 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. -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). +## 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. -# Links between Inventory Module and Other Modules +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. -This document showcases the module links defined between the Inventory Module and other commerce modules. + +# 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. + + +# Links between Inventory Module and Other Modules + +This document showcases the module links defined between the Inventory Module and other commerce modules. ## Summary @@ -19956,363 +19014,584 @@ const { data: inventoryLevels } = useQueryGraphStep({ ``` -# Order Concepts +# Inventory Kits -In this document, you’ll learn about orders and related concepts +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. -## Order Items +## What is an Inventory Kit? -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. +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. -![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) +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. -### Item’s Product Details +Using inventory kits, you can implement use cases like: -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. +- [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. *** -## 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). +## Multi-Part Products -![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) +Consider your store sells bicycles that consist of a frame, wheels, and seats, and you want to manage the inventory of these parts separately. -### data Property +To implement this in Medusa, you can: -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. +- 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. -The `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. +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. -The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. +![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 -## Order Totals +Using the Medusa Admin, you can create a multi-part product by creating its inventory items first, then assigning these inventory items to the product's variant(s). -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). +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" -## Order Payments +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", + }, + }) -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). + 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, + }, + ], + }, + ], + }, + }) -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. + // TODO create the product + } +) +``` -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). +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. -# Order Edit +Next, create the product and pass the inventory item's IDs to the product's variant: -In this document, you'll learn about order edits. +```ts highlights={multiPartHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + // ... + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -## What is an Order Edit? +export const createMultiPartProductsWorkflow = createWorkflow( + "create-multi-part-products", + () => { + // ... -A merchant can edit an order to add new items or change the quantity of existing items in the order. + const inventoryItemIds = transform({ + inventoryItems, + }, (data) => { + return data.inventoryItems.map((inventoryItem) => { + return { + inventory_item_id: inventoryItem.id, + // can also specify required_quantity + } + }) + }) -An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). + 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"], + }, + ], + }, + ], + }, + }) + } +) +``` -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. +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. -In the case of an order edit, the `OrderChange`'s type is `edit`. +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). *** -## 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. +## Bundled Products -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. +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) -## Update Items in an Order Edit +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. -A merchant can update an existing item's quantity or price. +Then, when the bundled product's variant is purchased, the inventory quantity of the associated inventory items are updated. -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. +![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 -## Shipping Methods of New Items in the Edit +You can create a bundled product in the Medusa Admin 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. -Adding new items to the order requires adding shipping methods for those items. +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: -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` - -*** +```ts highlights={bundledHighlights1} +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -## How Order Edits Impact an Order’s Version +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Shirt", + 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", + 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", + variants: [ + { + title: "Shoes", + prices: [ + { + amount: 10, + currency_code: "usd", + }, + ], + options: { + "Default Option": "Default Variant", + }, + manage_inventory: true, + }, + ], + options: [ + { + title: "Default Option", + values: ["Default Variant"], + }, + ], + }, + ], + }, + }) -When an order edit is confirmed, the order’s version is incremented. + // 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). -## Payments and Refunds for Order Edit Changes +Next, retrieve the products again but with variant information: -Once the Order Edit is confirmed, any additional payment or refund required can be made on the original order. +```ts highlights={bundledHighlights2} +import { + // ... + transform, +} from "@medusajs/framework/workflows-sdk" +import { + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" -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). +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, + }, + }) -# Order Claim + const inventoryItemIds = transform({ + productsWithInventory, + }, (data) => { + return data.productsWithInventory.map((product) => { + return { + inventory_item_id: product.variants[0].inventory_items?.[0]?.inventory_item_id, + } + }) + }) -In this document, you’ll learn about order claims. + // create bundled product + } +) +``` -## What is a Claim? +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. -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. +Finally, create the bundled product: -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. +```ts highlights={bundledProductHighlights3} +export const createBundledProducts = createWorkflow( + "create-bundled-products", + () => { + // ... + const bundledProduct = createProductsWorkflow.runAsStep({ + input: { + products: [ + { + title: "Bundled Clothes", + 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. -## Claim Type +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). -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. +# Links between Currency Module and Other Modules -*** +This document showcases the module links defined between the Currency Module and other commerce modules. -## Old and Replacement Items +## Summary -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. +The Currency Module has the following links to other modules: -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). +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. -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). +- [`Currency` data model of Store Module \<> `Currency` data model of Currency Module](#store-module). (Read-only). *** -## 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). +## Store Module -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). +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's `Currency` data model and the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `Currency` 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` data model in the Store Module. -## Claim Refund +### Retrieve with Query -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. +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 [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. +### query.graph -*** +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -## How Claims Impact an Order’s Version +// stores.supported_currencies +``` -When a claim is confirmed, the order’s version is incremented. +### useQueryGraphStep +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -# Order Exchange +// ... -In this document, you’ll learn about order exchanges. +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) -## What is an Exchange? +// stores.supported_currencies +``` -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. +# Fulfillment Concepts -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. +In this document, you’ll learn about some basic fulfillment concepts. -*** +## Fulfillment Set -## Returned and New Items +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. -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. +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. -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. +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` *** -## 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). - -*** +## Service Zone -## Exchange Payment +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 `Exchange` data model has a `difference_due` property that stores the outstanding amount. +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. -|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.| +![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) -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). +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. *** -## How Exchanges Impact an Order’s Version +## Shipping Profile -When an exchange is confirmed, the order’s version is incremented. +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. -# Links between Order Module and Other Modules -This document showcases the module links defined between the Order Module and other commerce modules. +# Fulfillment Module Provider -## Summary +In this document, you’ll learn what a fulfillment module provider is. -The Order Module has the following links to other modules: +## What’s a Fulfillment Module Provider? -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +A fulfillment module provider handles fulfilling items, typically using a third-party integration. -- [`Order` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). -- [`Order` data model \<> `Cart` data model of Cart Module](#cart-module). -- [`Order` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Return` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). -- [`Order` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderClaim` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`OrderExchange` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). -- [`Order` data model \<> `Product` data model of Product Module](#product-module). (Read-only). -- [`Order` data model \<> `Promotion` data model of Promotion Module](#promotion-module). -- [`Order` data model \<> `Region` data model of Region Module](#region-module). (Read-only). -- [`Order` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). +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). *** -## 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. +## Configure Fulfillment Providers -### Retrieve with Query +The Fulfillment Module accepts a `providers` option that allows you to register providers in your application. -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`: +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). -### query.graph +*** -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) +## How to Create a Fulfillment Provider? -// orders.customer -``` +Refer to [this guide](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) to learn how to create a fulfillment module provider. -### useQueryGraphStep -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +# Item Fulfillment -// ... +In this document, you’ll learn about the concepts of item fulfillment. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) +## Fulfillment Data Model -// orders.customer -``` +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). *** -## Cart Module +## Fulfillment Processing by a Fulfillment Provider -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 fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. -Medusa defines a link between the `Order` and `Cart` data models. The order is linked to the cart used for the purchased. +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. -![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) +![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) -### 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`: +## data Property -### query.graph +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "cart.*", - ], -}) +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. -// orders.cart -``` +*** -### useQueryGraphStep +## Fulfillment Items -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "cart.*", - ], -}) +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) -// orders.cart -``` +*** -### Manage with Link +## Fulfillment Label -To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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. -### link.create +*** -```ts -import { Modules } from "@medusajs/framework/utils" +## Fulfillment Status -// ... +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.CART]: { - cart_id: "cart_123", - }, -}) -``` +- `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. -### createRemoteLinkStep -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +# Links between Fulfillment Module and Other Modules -// ... +This document showcases the module links defined between the Fulfillment Module and other commerce modules. -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.CART]: { - cart_id: "cart_123", - }, -}) -``` +## Summary + +The Fulfillment Module has the following links to other modules: + +- [`Order` data model of the Order Module \<> `Fulfillment` data model](#order-module). +- [`Return` data model of the Order Module \<> `Fulfillment` data model](#order-module). +- [`PriceSet` data model of the Pricing Module \<> `ShippingOption` data model](#pricing-module). +- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentProvider` data model](#stock-location-module). +- [`StockLocation` data model of the Stock Location Module \<> `FulfillmentSet` data model](#stock-location-module). *** -## Fulfillment Module +## Order Module -A fulfillment is created for an orders' items. Medusa defines a link between the `Fulfillment` and `Order` data models. +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) @@ -20322,21 +19601,21 @@ A fulfillment is also created for a return's items. So, Medusa defines a link be ### 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 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 fulfillments of a return, pass `fulfillments.*` in `fields`. +To retrieve the return, pass `return.*` in `fields`. ### query.graph ```ts -const { data: orders } = await query.graph({ - entity: "order", +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", fields: [ - "fulfillments.*", + "order.*", ], }) -// orders.fulfillments +// fulfillments.order ``` ### useQueryGraphStep @@ -20346,19 +19625,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", fields: [ - "fulfillments.*", + "order.*", ], }) -// orders.fulfillments +// fulfillments.order ``` ### 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): +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -20397,29 +19676,29 @@ createRemoteLinkStep({ *** -## Payment Module +## Pricing Module -An order's payment details are stored in a payment collection. This also applies for claims and exchanges. +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. -So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. +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 Order and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716554726/Medusa%20Resources/order-payment_ubdwok.jpg) +![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 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`: +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: orders } = await query.graph({ - entity: "order", +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", fields: [ - "payment_collections.*", + "price_set.*", ], }) -// orders.payment_collections +// shippingOptions.price_set ``` ### useQueryGraphStep @@ -20429,19 +19708,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", fields: [ - "payment_collections.*", + "price_set.*", ], }) -// orders.payment_collections +// shippingOptions.price_set ``` ### 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): +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 @@ -20451,11 +19730,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -20469,83 +19748,46 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", + [Modules.PRICING]: { + price_set_id: "pset_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.*", - ], -}) +## Stock Location Module -// lineItems.variant -``` +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. -## Promotion Module +![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) -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. +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 Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) +![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 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 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: orders } = await query.graph({ - entity: "order", +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", fields: [ - "promotion.*", + "location.*", ], }) -// orders.promotion +// fulfillmentSets.location ``` ### useQueryGraphStep @@ -20555,19 +19797,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: orders } = useQueryGraphStep({ - entity: "order", +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", fields: [ - "promotion.*", + "location.*", ], }) -// orders.promotion +// fulfillmentSets.location ``` ### 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 location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -20577,11 +19819,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` @@ -20595,482 +19837,419 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", }, }) ``` -*** -## Region Module +# Shipping Option -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. +In this document, you’ll learn about shipping options and their rules. -### Retrieve with Query +## What’s a Shipping Option? -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`: +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. -### query.graph +When the customer places their order, they choose a shipping option to be used to fulfill their items. -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). -// orders.region -``` +*** -### useQueryGraphStep +## Service Zone Restrictions -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) +![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) -// orders.region -``` +Service zones can be more restrictive, such as restricting to certain cities or province codes. + +![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) *** -## Sales Channel Module +## Shipping Option Rules -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. +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. -### Retrieve with Query +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: -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`: +- `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. -### query.graph +![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) -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +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. -// orders.sales_channel -``` +![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) -### useQueryGraphStep +*** -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +## 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). -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "sales_channel.*", - ], -}) +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. -// orders.sales_channel -``` +*** +## data Property -# Order Change +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. -In this document, you'll learn about the Order Change data model and possible actions in it. +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -## 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. +# Fulfillment Module Options -Its `change_type` property indicates what the order change is created for: +In this document, you'll learn about the options of the Fulfillment Module. -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). +## providers -Once the order change is confirmed, its changes are applied on the order. +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. -## Order Change Actions +For example: -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). +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -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. +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` -|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\`| +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. -# Order Versioning -In this document, you’ll learn how an order and its details are versioned. +# Order Concepts -## What's Versioning? +In this document, you’ll learn about orders and related concepts -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. +## Order Items -When changes are made on an order, such as an item is added or returned, the order's version changes. +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) -## version Property +### Item’s Product Details -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. +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. *** -## 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. +## Order’s Shipping Method -When the order is retrieved, only the related data having the same version is retrieved. +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). -# Promotions Adjustments in Orders +![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) -In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. +### data Property -## What are Adjustment Lines? +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. -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 `OrderShippingMethod` data model has a `data` property. It’s an object used to store custom data relevant later for fulfillment. -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. +The Medusa application passes the `data` property to the Fulfillment Module when fulfilling items. -![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. +## Order Totals -The ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. +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). *** -## 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. +## 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). -## Promotion Actions +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. -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 transactions in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions/index.html.md). -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" +# Order Claim -// ... +In this document, you’ll learn about order claims. -// 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, - }) - } -}) +## What is a Claim? -//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, - }) - } -}) +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - // TODO infer from cart or region - currency_code: "usd", - } -) -``` +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. -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. +## Claim Type -```ts collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" +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. -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[] -) -``` +## 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. -# Tax Lines in Order Module +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). -In this document, you’ll learn about tax lines in an order. +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). -## What are Tax Lines? +*** -A tax line indicates the tax rate of a line item or a shipping method. +## Claim Shipping Methods -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 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). -![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) +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). *** -## Tax Inclusivity +## Claim Refund -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. +If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. -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. +The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. -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. +## How Claims Impact an Order’s Version -![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) +When a claim is confirmed, the order’s version is incremented. -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`. +# Order Edit -# Transactions +In this document, you'll learn about order edits. -In this document, you’ll learn about an order’s transactions and its use. +## What is an Order Edit? -## What is a Transaction? +A merchant can edit an order to add new items or change the quantity of existing items in the order. -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). +An order edit is represented by the [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md). -The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. +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. -Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. +In the case of an order edit, the `OrderChange`'s type is `edit`. *** -## 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. +## Add Items in an Order Edit -```json -{ - "totals": { - "total": 30, - "subtotal": 30, - // ... - } -} -``` +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). -To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: +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. -|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.| +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. *** -## 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). +## Update Items in an Order Edit -The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: +A merchant can update an existing item's quantity or price. -- `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`. +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. +*** -# Pricing Concepts +## Shipping Methods of New Items in the Edit -In this document, you’ll learn about the main concepts in the Pricing Module. +Adding new items to the order requires adding shipping methods for those items. -## Price Set +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` -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). +## How Order Edits Impact an Order’s Version -![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) +When an order edit is confirmed, the order’s version is incremented. *** -## 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. +## Payments and Refunds for Order Edit Changes -Its associated prices are represented by the `Price` data model. +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 Return -In this document, you’ll learn about order returns. +# Order Exchange -## What is a Return? +In this document, you’ll learn about order exchanges. -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). +## What is an Exchange? -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. +An exchange is the replacement of an item that the customer ordered with another. -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. -Once the merchant receives the returned items, they mark the return as received. +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. *** -## Returned Items +## Returned and New Items -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). +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. -The `ReturnItem` model has two properties storing the item's quantity: +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). -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. +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. *** -## Return Shipping Methods +## Exchange 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). +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). -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. +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). *** -## Refund Payment - -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. +## Exchange Payment -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. +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). *** -## Returns in Exchanges and Claims +## How Exchanges Impact an Order’s Version -When a merchant creates an exchange or a claim, it includes returning items from the customer. +When an exchange is confirmed, the order’s version is incremented. -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. + +# 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. + +- [`Order` data model \<> `Customer` data model of Customer Module](#customer-module). (Read-only). +- [`Order` data model \<> `Cart` data model of Cart Module](#cart-module). +- [`Order` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). +- [`Return` data model \<> `Fulfillment` data model of Fulfillment Module](#fulfillment-module). +- [`Order` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). +- [`OrderClaim` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). +- [`OrderExchange` data model \<> `PaymentCollection` data model of Payment Module](#payment-module). +- [`Order` data model \<> `Product` data model of Product Module](#product-module). (Read-only). +- [`Order` data model \<> `Promotion` data model of Promotion Module](#promotion-module). +- [`Order` data model \<> `Region` data model of Region Module](#region-module). (Read-only). +- [`Order` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). (Read-only). *** -## How Returns Impact an Order’s Version +## Customer Module -The order’s version is incremented when: +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. -1. A return is requested. -2. A return is marked as received. +### 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`: -# Links between Pricing Module and Other Modules +### query.graph -This document showcases the module links defined between the Pricing Module and other commerce modules. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) -## Summary +// orders.customer +``` -The Pricing Module has the following links to other modules: +### useQueryGraphStep -- [`ShippingOption` data model of Fulfillment Module \<> `PriceSet` data model](#fulfillment-module). -- [`ProductVariant` data model of Product Module \<> `PriceSet` data model](#product-module). +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` *** -## Fulfillment Module +## Cart 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. +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 `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. +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 Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.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) ### 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`: +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: priceSets } = await query.graph({ - entity: "price_set", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "shipping_option.*", + "cart.*", ], }) -// priceSets.shipping_option +// orders.cart ``` ### useQueryGraphStep @@ -21080,19 +20259,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "shipping_option.*", + "cart.*", ], }) -// priceSets.shipping_option +// orders.cart ``` ### 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): +To manage the cart of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21102,11 +20281,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.CART]: { + cart_id: "cart_123", }, }) ``` @@ -21120,44 +20299,44 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.CART]: { + cart_id: "cart_123", }, }) ``` *** -## Product Module - -The Product Module doesn't store or manage the prices of product variants. +## Fulfillment Module -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 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 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) +![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) -So, when you want to add prices for a product variant, you create a price set and add the prices to it. +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. -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. +![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 variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: +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: priceSets } = await query.graph({ - entity: "price_set", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "variant.*", + "fulfillments.*", ], }) -// priceSets.variant +// orders.fulfillments ``` ### useQueryGraphStep @@ -21167,19 +20346,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "variant.*", + "fulfillments.*", ], }) -// priceSets.variant +// orders.fulfillments ``` ### 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): +To manage the fulfillments of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21189,11 +20368,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` @@ -21207,528 +20386,671 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.ORDER]: { + order_id: "order_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", }, }) ``` +*** -# Price Rules +## Payment Module -In this document, you'll learn about price rules for price sets and price lists. +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -## Price Rule +So, Medusa defines links between the `PaymentCollection` data model and the `Order`, `OrderClaim`, and `OrderExchange` data models. -You can restrict prices by rules. Each rule of a price is represented by the [PriceRule data model](https://docs.medusajs.com/references/pricing/models/PriceRule/index.html.md). +![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) -The `Price` data model has a `rules_count` property, which indicates how many rules, represented by `PriceRule`, are applied to the price. +### Retrieve with Query -For exmaple, you create a price restricted to `10557` zip codes. +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`: -![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) +### query.graph -A price can have multiple price rules. +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) -For example, a price can be restricted by a region and a zip code. +// orders.payment_collections +``` -![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) +### useQueryGraphStep -*** +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -## 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). +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "payment_collections.*", + ], +}) -The `rules_count` property of a `PriceList` indicates how many rules are applied to it. +// orders.payment_collections +``` -![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) +### 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): -# Tax-Inclusive Pricing +### link.create -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. +```ts +import { Modules } from "@medusajs/framework/utils" -## 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. +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -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. +### createRemoteLinkStep -*** +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -## How is Tax-Inclusive Pricing Set? +// ... -The [PricePreference data model](https://docs.medusajs.com/references/pricing/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` -- `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. +## Product Module -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. +Medusa defines read-only links between: -For example: - -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} -``` +- 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. -In this example, tax-inclusivity is enabled for the `USD` currency code. +### 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`: -## Tax-Inclusive Pricing in Price Calculation +To retrieve the product, pass `product.*` in `fields`. -### Tax Context +### query.graph -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. +```ts +const { data: lineItems } = await query.graph({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. +// lineItems.variant +``` -### Returned Tax Properties +### useQueryGraphStep -The `calculatePrices` method returns two properties related to tax-inclusivity: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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. +const { data: lineItems } = useQueryGraphStep({ + entity: "order_line_item", + fields: [ + "variant.*", + ], +}) -A price is considered tax-inclusive if: +// lineItems.variant +``` -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 +## Promotion Module -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. -- 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 +![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 -# Prices Calculation +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`: -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. +### query.graph -## calculatePrices Method +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "promotion.*", + ], +}) -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. +// orders.promotion +``` -It returns a price object with the best matching price for each price set. +### useQueryGraphStep -### Calculation Context +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -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: +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "promotion.*", + ], +}) -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSetId] }, - { - context: { - currency_code: currencyCode, - region_id: "reg_123", - }, - } -) +// orders.promotion ``` -In this example, you retrieve the prices in a price set for the specified currency code and region ID. +### Manage with Link -### Returned Price Object +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -For each price set, the `calculatePrices` method selects two prices: +### link.create -- 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. +```ts +import { Modules } from "@medusajs/framework/utils" -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. +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` - - id: (\`string\`) The ID of the price. +### createRemoteLinkStep - - price\_list\_id: (\`string\`) The ID of the associated price list. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +// ... - - min\_quantity: (\`number\`) The price's min quantity condition. +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` - - 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. +## Region Module - - price\_list\_id: (\`string\`) The ID of the associated price list. +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. - - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. +### Retrieve with Query - - min\_quantity: (\`number\`) The price's min quantity condition. +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`: - - max\_quantity: (\`number\`) The price's max quantity condition. +### query.graph -*** +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "region.*", + ], +}) -## Examples +// orders.region +``` -Consider the following price set: +### useQueryGraphStep ```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", - }, - }, +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "region.*", ], }) + +// orders.region ``` -### Default Price Selection +*** -### Code +## Sales Channel Module -```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR" - } - } -) -``` +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. -### Result +### Retrieve with Query -### Calculate Prices with Rules +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`: -### Code +### query.graph ```ts -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "reg_123", - city: "krakow" - } - } -) +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "sales_channel.*", + ], +}) + +// orders.sales_channel ``` -### Result +### useQueryGraphStep -### Price Selection with Price List +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### 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 { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "sales_channel.*", ], -}]); +}) -const price = await pricingModuleService.calculatePrices( - { id: [priceSet.id] }, - { - context: { - currency_code: "EUR", - region_id: "PL", - city: "krakow" - } - } -) +// orders.sales_channel ``` -### Result +# Order Versioning -# Links between Product Module and Other Modules +In this document, you’ll learn how an order and its details are versioned. -This document showcases the module links defined between the Product Module and other commerce modules. +## What's Versioning? -## Summary +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. -The Product Module has the following links to other modules: +When changes are made on an order, such as an item is added or returned, the order's version changes. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +*** -- [`Product` data model \<> `Cart` data model of Cart Module](#cart-module). (Read-only). -- [`ProductVariant` data model \<> `InventoryItem` data model of Inventory Module](#inventory-module). -- [`Product` data model \<> `Order` data model of Order Module](#order-module). (Read-only). -- [`ProductVariant` data model \<> `PriceSet` data model of Pricing Module](#pricing-module). -- [`Product` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). +## version Property -*** +The `Order` and `OrderSummary` data models have a `version` property that indicates the current version. By default, its value is `1`. -## Cart Module +Other order-related data models, such as `OrderItem`, also has a `version` property, but it indicates the version it belongs to. -Medusa defines read-only links between: +*** -- The `Product` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` 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 `LineItem` data model. -- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` 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 `LineItem` data model. +## How the Version Changes -### Retrieve with Query +When the order is changed, such as an item is exchanged, this changes the version of the order and its related data: -To retrieve the line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `line_items.*` in `fields`: +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. -To retrieve the line items of a product, pass `line_items.*` in `fields`. +When the order is retrieved, only the related data having the same version is retrieved. -### query.graph -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "line_items.*", - ], -}) +# Promotions Adjustments in Orders -// variants.line_items -``` +In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. -### useQueryGraphStep +## What are Adjustment Lines? -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "line_items.*", - ], -}) +![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) -// variants.line_items -``` +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. *** -## Inventory Module +## Discountable Option -The Inventory Module provides inventory-management features for any stock-kept item. +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. -Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. +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. -![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) +*** -When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. +## Promotion Actions -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). +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. -### Retrieve with Query +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). -To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: +```ts collapsibleLines="1-10" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" -### query.graph +// ... -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "inventory_items.*", +// 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, + }) + } +}) -// variants.inventory_items +//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", + } +) ``` -### useQueryGraphStep +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "inventory_items.*", - ], -}) +await orderModuleService.setOrderLineItemAdjustments( + order.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) -// variants.inventory_items +await orderModuleService.setOrderShippingMethodAdjustments( + order.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) ``` -### Manage with Link -To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +# Order Change -### link.create +In this document, you'll learn about the Order Change data model and possible actions in it. -```ts -import { Modules } from "@medusajs/framework/utils" +## 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. -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` +Its `change_type` property indicates what the order change is created for: -### createRemoteLinkStep +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). -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +Once the order change is confirmed, its changes are applied on the order. -// ... +*** -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.INVENTORY]: { - inventory_item_id: "iitem_123", - }, -}) -``` +## 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 Return + +In this document, you’ll learn about order returns. + +## 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. *** -## Order Module +## Returned Items -Medusa defines read-only links between: +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). -- the `Product` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` 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 `ProductVariant` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` 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. +The `ReturnItem` model has two properties storing the item's quantity: -### Retrieve with Query +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. -To retrieve the order line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order_items.*` in `fields`: +*** -To retrieve a product's order line items, pass `order_items.*` in `fields`. +## Return Shipping Methods -### query.graph +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). -```ts -const { data: variants } = await query.graph({ - entity: "variant", - fields: [ - "order_items.*", - ], -}) +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. -// variants.order_items -``` +*** -### useQueryGraphStep +## Refund Payment -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: variants } = useQueryGraphStep({ - entity: "variant", - fields: [ - "order_items.*", - ], -}) +*** -// variants.order_items +## 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.| + *** -## Pricing Module +## Transaction Reference -The Product Module doesn't provide pricing-related features. +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). -Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set. +The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) +- `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`. -So, to add prices for a product variant, create a price set and add the prices to it. + +# Links between Payment Module and Other Modules + +This document showcases the module links defined between the Payment Module and other commerce modules. + +## Summary + +The Payment Module has the following links to other modules: + +- [`Cart` data model of Cart Module \<> `PaymentCollection` data model](#cart-module). +- [`Order` data model of Order Module \<> `PaymentCollection` data model](#order-module). +- [`OrderClaim` data model of Order Module \<> `PaymentCollection` data model](#order-module). +- [`OrderExchange` data model of Order Module \<> `PaymentCollection` data model](#order-module). +- [`Region` data model of Region Module \<> `PaymentProvider` data model](#region-module). + +*** + +## Cart Module + +The Cart Module provides cart-related features, but not payment processing. + +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. + +Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md). ### Retrieve with Query -To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: ### query.graph ```ts -const { data: variants } = await query.graph({ - entity: "variant", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "price_set.*", + "cart.*", ], }) -// variants.price_set +// paymentCollections.cart ``` ### useQueryGraphStep @@ -21738,19 +21060,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: variants } = useQueryGraphStep({ - entity: "variant", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "price_set.*", + "cart.*", ], }) -// variants.price_set +// paymentCollections.cart ``` ### 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): +To manage the payment collection of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -21760,11 +21082,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` @@ -21772,46 +21094,45 @@ await link.create({ ### createRemoteLinkStep ```ts -import { Modules } from "@medusajs/framework/utils" import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", + [Modules.CART]: { + cart_id: "cart_123", }, - [Modules.PRICING]: { - price_set_id: "pset_123", + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", }, }) ``` *** -## Sales Channel Module +## Order Module -The Sales Channel Module provides functionalities to manage multiple selling channels in your store. +An order's payment details are stored in a payment collection. This also applies for claims and exchanges. -Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. +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 Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) +![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 sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: +To retrieve the order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: ### query.graph ```ts -const { data: products } = await query.graph({ - entity: "product", +const { data: paymentCollections } = await query.graph({ + entity: "payment_collection", fields: [ - "sales_channels.*", + "order.*", ], }) -// products.sales_channels +// paymentCollections.order ``` ### useQueryGraphStep @@ -21821,262 +21142,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = useQueryGraphStep({ - entity: "product", +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", fields: [ - "sales_channels.*", + "order.*", ], }) -// products.sales_channels +// paymentCollections.order ``` ### Manage with Link -To manage the sales channels 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.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - - -# Product Variant Inventory - -# Product Variant Inventory - -In this guide, you'll learn about the inventory management features related to product variants. - -## Configure Inventory Management of Product Variants - -A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant. - -The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. - -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. - -*** - -## How the Medusa Application Manages Inventory - -When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. - -![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) - -The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. - -![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) - -Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). - -The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. - -![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) - -Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). - -### Product Inventory in Storefronts - -When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. - -The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. - -For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. - -![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) - -*** - -## Variant Back Orders - -Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. - -You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). - -*** - -## Additional Resources - -The following guides provide more details on inventory management in the Medusa application: - -- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. -- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. -- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). - - -# Links between Payment Module and Other Modules - -This document showcases the module links defined between the Payment Module and other commerce modules. - -## Summary - -The Payment Module has the following links to other modules: - -- [`Cart` data model of Cart Module \<> `PaymentCollection` data model](#cart-module). -- [`Order` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`OrderClaim` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`OrderExchange` data model of Order Module \<> `PaymentCollection` data model](#order-module). -- [`Region` data model of Region Module \<> `PaymentProvider` data model](#region-module). - -*** - -## Cart Module - -The Cart Module provides cart-related features, but not payment processing. - -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. - -Learn more about this relation in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-collection#usage-with-the-cart-module/index.html.md). - -### Retrieve with Query - -To retrieve the cart associated with the payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `cart.*` in `fields`: - -### query.graph - -```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) - -// paymentCollections.cart -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) - -// paymentCollections.cart -``` - -### Manage with Link - -To manage the payment collection 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.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -*** - -## Order 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 order of a payment collection with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -### query.graph - -```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", - fields: [ - "order.*", - ], -}) - -// paymentCollections.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "order.*", - ], -}) - -// paymentCollections.order -``` - -### 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): +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 @@ -22287,103 +21365,17 @@ A payment can be refunded multiple times, and each time a refund record is creat ![A diagram showcasing how a payment's multiple refunds are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565555/Medusa%20Resources/payment-refund_lgfvyy.jpg) -# Payment Collection +# Accept Payment Flow -In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. +In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. -## What's a Payment Collection? +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. -A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). +For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). -Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: +## Flow Overview -- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. -- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. -- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. - -*** - -## Multiple Payments - -The payment collection supports multiple payment sessions and payments. - -You can use this to accept payments in increments or split payments across payment providers. - -![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) - -*** - -## Usage with the Cart Module - -The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. - -During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. - -It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). - -![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) - - -# Payment Module Provider - -In this document, you’ll learn what a payment module provider is. - -## What's a Payment Module Provider? - -A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. - -To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. - -After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. - -### List of Payment Module Providers - -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) - -*** - -## System Payment Provider - -The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. - -It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. - -*** - -## How are Payment Providers Created? - -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. - -Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. - -*** - -## Configure Payment Providers - -The Payment Module accepts a `providers` option that allows you to register providers in your application. - -Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). - -*** - -## PaymentProvider Data Model - -When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. - -This data model is used to reference a payment provider and determine whether it’s installed in the application. - - -# Accept Payment Flow - -In this document, you’ll learn how to implement an accept-payment flow using workflows or the Payment Module's main service. - -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. - -For a guide on how to implement this flow in the storefront, check out [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/index.html.md). - -## Flow Overview - -![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) +![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) *** @@ -22540,40 +21532,90 @@ 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. -# Webhook Events +# Payment Module Provider -In this document, you’ll learn how the Payment Module supports listening to webhook events. +In this document, you’ll learn what a payment module provider is. -## What's a Webhook Event? +## What's a Payment Module Provider? -A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. +A payment module provider registers a payment provider that handles payment processing in the Medusa application. It integrates third-party payment providers, such as Stripe. -This is useful in many cases such as when a payment is being processed asynchronously or when a request is interrupted and the payment provider is sending details on the process later. +To authorize a payment amount with a payment provider, a payment session is created and associated with that payment provider. The payment provider is then used to handle the authorization. + +After the payment session is authorized, the payment provider is associated with the resulting payment and handles its payment processing, such as to capture or refund payment. + +### List of Payment Module Providers + +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) *** -## getWebhookActionAndData Method +## System Payment Provider -The Payment Module’s main service has a [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) used to handle incoming webhook events from third-party payment services. The method delegates the handling to the associated payment provider, which returns the event's details. +The Payment Module provides a `system` payment provider that acts as a placeholder payment provider. -Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: +It doesn’t handle payment processing and delegates that to the merchant. It acts similarly to a cash-on-delivery (COD) payment method. -- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. -- `[provider]` is the ID of the provider. For example, `stripe`. +*** -For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. If you're integrating Stripe's Bancontact payments, the webhook listener route is `/hooks/payment/stripe-bancontact_stripe`. +## How are Payment Providers Created? -Use that webhook listener in your third-party payment provider's configurations. +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. -![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) +Refer to [this guide](https://docs.medusajs.com/references/payment/provider/index.html.md) on how to create a payment provider for the Payment Module. -If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. +*** -If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. +## Configure Payment Providers -### Actions After Webhook Payment Processing +The Payment Module accepts a `providers` option that allows you to register providers in your application. -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. +Learn more about this option in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options#providers/index.html.md). + +*** + +## PaymentProvider Data Model + +When the Medusa application starts and registers the payment providers, it also creates a record of the `PaymentProvider` data model if none exists. + +This data model is used to reference a payment provider and determine whether it’s installed in the application. + + +# Payment Collection + +In this document, you’ll learn what a payment collection is and how the Medusa application uses it with the Cart Module. + +## What's a Payment Collection? + +A payment collection stores payment details related to a resource, such as a cart or an order. It’s represented by the [PaymentCollection data model](https://docs.medusajs.com/references/payment/models/PaymentCollection/index.html.md). + +Every purchase or request for payment starts with a payment collection. The collection holds details necessary to complete the payment, including: + +- The [payment sessions](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) that represents the payment amount to authorize. +- The [payments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment/index.html.md) that are created when a payment session is authorized. They can be captured and refunded. +- The [payment providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) that handle the processing of each payment session, including the authorization, capture, and refund. + +*** + +## Multiple Payments + +The payment collection supports multiple payment sessions and payments. + +You can use this to accept payments in increments or split payments across payment providers. + +![Diagram showcasing how a payment collection can have multiple payment sessions and payments](https://res.cloudinary.com/dza7lstvk/image/upload/v1711554695/Medusa%20Resources/payment-collection-multiple-payments_oi3z3n.jpg) + +*** + +## Usage with the Cart Module + +The Cart Module provides cart management features. However, it doesn’t provide any features related to accepting payment. + +During checkout, the Medusa application links a cart to a payment collection, which will be used for further payment processing. + +It also implements the payment flow during checkout as explained in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md). + +![Diagram showcasing the relation between the Payment and Cart modules](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) # Payment Session @@ -22609,247 +21651,294 @@ The `status` property of a payment session indicates its current status. Its val - `canceled`: The authorization of the payment session has been canceled. -# Links between Region Module and Other Modules - -This document showcases the module links defined between the Region Module and other commerce modules. +# Webhook Events -## Summary +In this document, you’ll learn how the Payment Module supports listening to webhook events. -The Region Module has the following links to other modules: +## What's a Webhook Event? -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +A webhook event is sent from a third-party payment provider to your application. It indicates a change in a payment’s status. -- [`Region` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`Region` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Region` data model \<> `PaymentProvider` data model of the Payment Module](#payment-module). +This is useful in many cases such as when a payment is being processed asynchronously or when a request is interrupted and the payment provider is sending details on the process later. *** -## Cart Module +## getWebhookActionAndData Method -Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's carts, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. +The Payment Module’s main service has a [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) used to handle incoming webhook events from third-party payment services. The method delegates the handling to the associated payment provider, which returns the event's details. -### Retrieve with Query +Medusa implements a webhook listener route at the `/hooks/payment/[identifier]_[provider]` API route, where: -To retrieve the carts of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: +- `[identifier]` is the `identifier` static property defined in the payment provider. For example, `stripe`. +- `[provider]` is the ID of the provider. For example, `stripe`. -### query.graph +For example, when integrating basic Stripe payments with the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md), the webhook listener route is `/hooks/payment/stripe_stripe`. If you're integrating Stripe's Bancontact payments, the webhook listener route is `/hooks/payment/stripe-bancontact_stripe`. -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "carts.*", - ], -}) +Use that webhook listener in your third-party payment provider's configurations. -// regions.carts -``` +![A diagram showcasing the steps of how the getWebhookActionAndData method words](https://res.cloudinary.com/dza7lstvk/image/upload/v1711567415/Medusa%20Resources/payment-webhook_seaocg.jpg) -### useQueryGraphStep +If the event's details indicate that the payment should be authorized, then the [authorizePaymentSession method of the main service](https://docs.medusajs.com/references/payment/authorizePaymentSession/index.html.md) is executed on the specified payment session. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +If the event's details indicate that the payment should be captured, then the [capturePayment method of the main service](https://docs.medusajs.com/references/payment/capturePayment/index.html.md) is executed on the payment of the specified payment session. -// ... +### Actions After Webhook Payment Processing -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "carts.*", - ], -}) +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. -// regions.carts -``` -*** +# Pricing Concepts -## Order Module +In this document, you’ll learn about the main concepts in the Pricing Module. -Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's orders, 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. +## Price Set -### Retrieve with Query +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). -To retrieve the orders of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +Each of these prices are represented by the [Price data module](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). -### query.graph +![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) -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "orders.*", - ], -}) +*** -// regions.orders -``` +## Price List -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "orders.*", - ], -}) -// regions.orders -``` - -*** +# Prices Calculation -## Payment Module +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. -You can specify for each region which payment providers are available for use. +## calculatePrices Method -Medusa defines a module link between the `PaymentProvider` and the `Region` data models. +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. -![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) +It returns a price object with the best matching price for each price set. -### Retrieve with Query +### Calculation Context -To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: +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. -### query.graph +For example: ```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions.payment_providers +const price = await pricingModuleService.calculatePrices( + { id: [priceSetId] }, + { + context: { + currency_code: currencyCode, + region_id: "reg_123", + }, + } +) ``` -### useQueryGraphStep +In this example, you retrieve the prices in a price set for the specified currency code and region ID. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +### Returned Price Object -// ... +For each price set, the `calculatePrices` method selects two prices: -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) +- 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. -// regions.payment_providers -``` +Both prices are returned in an object that has the following properties: -### Manage with Link +- 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. -To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + - id: (\`string\`) The ID of the price. -### link.create + - price\_list\_id: (\`string\`) The ID of the associated price list. -```ts -import { Modules } from "@medusajs/framework/utils" + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -// ... + - min\_quantity: (\`number\`) The price's min quantity condition. -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` + - max\_quantity: (\`number\`) The price's max quantity condition. +- original\_price: (\`object\`) The original price's price details. -### createRemoteLinkStep + - id: (\`string\`) The ID of the price. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + - price\_list\_id: (\`string\`) The ID of the associated price list. -// ... + - price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`. -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, + - 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", + }, + }, + ], }) ``` +### Default Price Selection -# Stock Location Concepts +### Code -In this document, you’ll learn about the main concepts in the Stock Location Module. +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR" + } + } +) +``` -## Stock Location +### Result -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. +### Calculate Prices with Rules -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. +### Code -*** +```ts +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "reg_123", + city: "krakow" + } + } +) +``` -## StockLocationAddress +### Result -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. +### Price Selection with Price List +### Code -# Links between Stock Location Module and Other Modules +```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, + }, + ], +}]); -This document showcases the module links defined between the Stock Location Module and other commerce modules. +const price = await pricingModuleService.calculatePrices( + { id: [priceSet.id] }, + { + context: { + currency_code: "EUR", + region_id: "PL", + city: "krakow" + } + } +) +``` -## Summary +### Result -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. +# Links between Pricing Module and Other Modules -- [`FulfillmentSet` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). -- [`FulfillmentProvider` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). -- [`StockLocation` data model \<> `Inventory` data model of the Inventory Module](#inventory-module). -- [`SalesChannel` data model of the Sales Channel Module \<> `StockLocation` data model](#sales-channel-module). +This document showcases the module links defined between the Pricing Module and other commerce modules. -*** +## Summary -## Fulfillment Module +The Pricing Module has the following links to other modules: -A fulfillment set can be conditioned to a specific stock location. +- [`ShippingOption` data model of Fulfillment Module \<> `PriceSet` data model](#fulfillment-module). +- [`ProductVariant` data model of Product Module \<> `PriceSet` data model](#product-module). -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) +## Fulfillment Module -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. +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. -![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) +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -### Retrieve with Query +![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) -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`: +### Retrieve with Query -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. +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: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "fulfillment_sets.*", + "shipping_option.*", ], }) -// stockLocations.fulfillment_sets +// priceSets.shipping_option ``` ### useQueryGraphStep @@ -22859,19 +21948,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "fulfillment_sets.*", + "shipping_option.*", ], }) -// stockLocations.fulfillment_sets +// priceSets.shipping_option ``` ### 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): +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 @@ -22881,11 +21970,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -22899,80 +21988,44 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` *** -## Inventory Module - -Medusa defines a read-only link between the `StockLocation` data model and the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model. This means you can retrieve the details of a stock location's inventory levels, 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 inventory levels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_levels.*` in `fields`: - -### query.graph - -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) - -// stockLocations.inventory_levels -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "inventory_levels.*", - ], -}) - -// stockLocations.inventory_levels -``` +## Product Module -*** +The Product Module doesn't store or manage the prices of product variants. -## Sales Channel Module +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 stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +![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) -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +So, when you want to add prices for a product variant, you create a price set and add the prices to it. -![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) +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 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`: +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: stockLocations } = await query.graph({ - entity: "stock_location", +const { data: priceSets } = await query.graph({ + entity: "price_set", fields: [ - "sales_channels.*", + "variant.*", ], }) -// stockLocations.sales_channels +// priceSets.variant ``` ### useQueryGraphStep @@ -22982,19 +22035,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", fields: [ - "sales_channels.*", + "variant.*", ], }) -// stockLocations.sales_channels +// priceSets.variant ``` ### 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): +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 @@ -23004,11 +22057,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` @@ -23022,176 +22075,150 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PRICING]: { + price_set_id: "pset_123", }, }) ``` -# Links between Sales Channel Module and Other Modules - -This document showcases the module links defined between the Sales Channel Module and other commerce modules. - -## Summary +# Price Rules -The Sales Channel Module has the following links to other modules: +In this document, you'll learn about price rules for price sets and price lists. -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. +## Price Rule -- [`ApiKey` data model of the API Key Module \<> `SalesChannel` data model](#api-key-module). -- [`SalesChannel` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) -- [`SalesChannel` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) -- [`Product` data model of the Product Module \<> `SalesChannel` data model](#product-module). -- [`SalesChannel` data model \<> `StockLocation` data model of the Stock Location Module](#stock-location-module). +You can restrict prices by rules. 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. -## API Key Module +For exmaple, you create a price restricted to `10557` zip codes. -A publishable API key allows you to easily specify the sales channel scope in a client request. +![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) -Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. +A price can have multiple price rules. -![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) +For example, a price can be restricted by a region and a zip code. -### Retrieve with Query +![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) -To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: +*** -### query.graph +## Price List Rules -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) +Rules applied to a price list are represented by the [PriceListRule data model](https://docs.medusajs.com/references/pricing/models/PriceListRule/index.html.md). -// salesChannels.publishable_api_keys -``` +The `rules_count` property of a `PriceList` indicates how many rules are applied to it. -### useQueryGraphStep +![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) -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -// ... +# Tax-Inclusive Pricing -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "publishable_api_keys.*", - ], -}) +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. -// salesChannels.publishable_api_keys -``` +## What is Tax-Inclusive Pricing? -### Manage with Link +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. -To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): +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. -### link.create +*** -```ts -import { Modules } from "@medusajs/framework/utils" +## How is Tax-Inclusive Pricing Set? -// ... +The [PricePreference data model](https://docs.medusajs.com/references/pricing/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: -await link.create({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` +- `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`. -### createRemoteLinkStep +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. -// ... +For example: -createRemoteLinkStep({ - [Modules.API_KEY]: { - api_key_id: "apk_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} ``` +In this example, tax-inclusivity is enabled for the `USD` currency code. + *** -## Cart Module +## Tax-Inclusive Pricing in Price Calculation -Medusa defines a read-only link between the `SalesChannel` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a sales channel's carts, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +### Tax Context -### Retrieve with Query +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 retrieve the carts of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. -### query.graph +### Returned Tax Properties -```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", - fields: [ - "carts.*", - ], -}) +The `calculatePrices` method returns two properties related to tax-inclusivity: -// salesChannels.carts -``` +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). -### useQueryGraphStep +- `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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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. -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", - fields: [ - "carts.*", - ], -}) +### Tax Context Precedence -// salesChannels.carts -``` +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 -## Order Module -Medusa defines a read-only link between the `SalesChannel` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a sales channel's orders, 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. +# Links between Region Module and Other Modules + +This document showcases the module links defined between the Region Module and other commerce modules. + +## Summary + +The Region 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. + +- [`Region` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) +- [`Region` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) +- [`Region` data model \<> `PaymentProvider` data model of the Payment Module](#payment-module). + +*** + +## Cart Module + +Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's carts, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. ### Retrieve with Query -To retrieve the orders of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: +To retrieve the carts of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: regions } = await query.graph({ + entity: "region", fields: [ - "orders.*", + "carts.*", ], }) -// salesChannels.orders +// regions.carts ``` ### useQueryGraphStep @@ -23201,41 +22228,37 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: regions } = useQueryGraphStep({ + entity: "region", fields: [ - "orders.*", + "carts.*", ], }) -// salesChannels.orders +// regions.carts ``` *** -## Product Module - -A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. - -![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) +## Order Module -A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. +Medusa defines a read-only link between the `Region` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a region's orders, 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 products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: +To retrieve the orders of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: regions } = await query.graph({ + entity: "region", fields: [ - "products.*", + "orders.*", ], }) -// salesChannels.products +// regions.orders ``` ### useQueryGraphStep @@ -23245,80 +22268,41 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: regions } = useQueryGraphStep({ + entity: "region", fields: [ - "products.*", + "orders.*", ], }) -// salesChannels.products -``` - -### Manage with Link - -To manage the sales channels 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.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, -}) +// regions.orders ``` *** -## Stock Location Module +## Payment Module -A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. +You can specify for each region which payment providers are available for use. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +Medusa defines a module link between the `PaymentProvider` and the `Region` 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) +![A diagram showcasing an example of how resources from the Payment and Region modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711569520/Medusa%20Resources/payment-region_jyo2dz.jpg) ### Retrieve with Query -To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: +To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: ### query.graph ```ts -const { data: salesChannels } = await query.graph({ - entity: "sales_channel", +const { data: regions } = await query.graph({ + entity: "region", fields: [ - "stock_locations.*", + "payment_providers.*", ], }) -// salesChannels.stock_locations +// regions.payment_providers ``` ### useQueryGraphStep @@ -23328,19 +22312,19 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: salesChannels } = useQueryGraphStep({ - entity: "sales_channel", +const { data: regions } = useQueryGraphStep({ + entity: "region", fields: [ - "stock_locations.*", + "payment_providers.*", ], }) -// salesChannels.stock_locations +// regions.payment_providers ``` ### 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): +To manage the payment providers in a region, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): ### link.create @@ -23350,11 +22334,11 @@ import { Modules } from "@medusajs/framework/utils" // ... await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` @@ -23368,40 +22352,16 @@ import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", + [Modules.REGION]: { + region_id: "reg_123", }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", }, }) ``` -# Publishable API Keys with Sales Channels - -In this document, you’ll learn what publishable API keys are and how to use them with sales channels. - -## Publishable API Keys with Sales Channels - -A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. - -When sending a request to a Store API route, you must pass a publishable API key in the header of the request: - -```bash -curl http://localhost:9000/store/products \ - x-publishable-api-key: {your_publishable_api_key} -``` - -The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. - -*** - -## How to Create a Publishable API Key? - -To create a publishable API key, either use the Medusa Admin or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys/index.html.md). - - # Promotion Actions 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). @@ -23513,6 +22473,30 @@ export interface CampaignBudgetExceededAction { Refer to [this reference](https://docs.medusajs.com/references/promotion/interfaces/promotion.CampaignBudgetExceededAction/index.html.md) for details on the object’s properties. +# Campaign + +In this document, you'll learn about campaigns. + +## 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) + + # Application Method In this document, you'll learn what an application method is. @@ -23550,30 +22534,6 @@ The application method has a collection of `PromotionRule` items to define the 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. - -## 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 document, you’ll learn about the main promotion and rule concepts in the Promotion Module. @@ -23810,41 +22770,48 @@ createRemoteLinkStep({ ``` -# Links between Store Module and Other Modules +# Links between Product Module and Other Modules -This document showcases the module links defined between the Store Module and other commerce modules. +This document showcases the module links defined between the Product Module and other commerce modules. ## Summary -The Store Module has the following links to other modules: +The Product 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. -- [`Currency` data model \<> `Currency` data model of Currency Module](#currency-module). (Read-only). +- [`Product` data model \<> `Cart` data model of Cart Module](#cart-module). (Read-only). +- [`ProductVariant` data model \<> `InventoryItem` data model of Inventory Module](#inventory-module). +- [`Product` data model \<> `Order` data model of Order Module](#order-module). (Read-only). +- [`ProductVariant` data model \<> `PriceSet` data model of Pricing Module](#pricing-module). +- [`Product` data model \<> `SalesChannel` data model of Sales Channel Module](#sales-channel-module). *** -## Currency Module +## Cart 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. +Medusa defines read-only links between: -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 `Currency` 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` data model in the Store Module. +- The `Product` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` 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 `LineItem` data model. +- The `ProductVariant` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItem` 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 `LineItem` data model. ### 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`: +To retrieve the line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `line_items.*` in `fields`: + +To retrieve the line items of a product, pass `line_items.*` in `fields`. ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: variants } = await query.graph({ + entity: "variant", fields: [ - "supported_currencies.currency.*", + "line_items.*", ], }) -// stores.supported_currencies +// variants.line_items ``` ### useQueryGraphStep @@ -23854,869 +22821,1637 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "line_items.*", ], }) -// stores.supported_currencies +// variants.line_items ``` +*** -# Tax Module Options +## Inventory Module -In this document, you'll learn about the options of the Tax Module. +The Inventory Module provides inventory-management features for any stock-kept item. -## providers +Medusa defines a link between the `ProductVariant` and `InventoryItem` data models. Each product variant has different inventory details. -The `providers` option is an array of either tax module providers or path to a file that defines a tax provider. +![A diagram showcasing an example of how data models from the Product and Inventory modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. +When the `manage_inventory` property of a product variant is enabled, you can manage the variant's inventory in different locations through this relation. -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" +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 -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, +To retrieve the inventory items of a product variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_items.*` in `fields`: + +### query.graph + +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "inventory_items.*", ], }) + +// variants.inventory_items ``` -The objects in the array accept the following properties: +### useQueryGraphStep -- `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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +// ... -# Tax Calculation with the Tax Provider +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "inventory_items.*", + ], +}) -In this document, you’ll learn how tax lines are calculated and what a tax provider is. +// variants.inventory_items +``` -## Tax Lines Calculation +### Manage with Link -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. +To manage the inventory items of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -For example: +### link.create ```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", - }, - } -) +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) ``` -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. +### createRemoteLinkStep -The example above retrieves the tax lines based on the tax region for the United States. +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -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" +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", }, - { - "shipping_line_id": "casm_123", - "rate_id": "txr_2", - "rate": 5, - "code": "YYY", - "name": "Tax Rate 2" - } -] + [Modules.INVENTORY]: { + inventory_item_id: "iitem_123", + }, +}) ``` *** -## 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. +## Order Module -{/* --- +Medusa defines read-only links between: -TODO add once tax provider guide is updated + add module providers match other modules. +- the `Product` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` 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 `ProductVariant` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `OrderLineItem` 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. -## Create Tax Provider +### Retrieve with Query -Refer to [this guide](/modules/tax/provider) to learn more about creating a tax provider. */} +To retrieve the order line items of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order_items.*` in `fields`: +To retrieve a product's order line items, pass `order_items.*` in `fields`. -# Tax Rates and Rules +### query.graph -In this document, you’ll learn about tax rates and rules. +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "order_items.*", + ], +}) -## What are Tax Rates? +// variants.order_items +``` -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. +### useQueryGraphStep -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -### 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`. +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "order_items.*", + ], +}) -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. +// variants.order_items +``` *** -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. +## Pricing Module -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. +The Product Module doesn't provide pricing-related features. -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. +Instead, Medusa defines a link between the `ProductVariant` and the `PriceSet` data models. A product variant’s prices are stored belonging to a price set. -![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) +![A diagram showcasing an example of how data models from the Pricing and Product Module are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651464/Medusa%20Resources/product-pricing_vlxsiq.jpg) -These two properties of the data model identify the rule’s target: +So, to add prices for a product variant, create a price set and add the prices to it. -- `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. +### Retrieve with Query -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. +To retrieve the price set of a variant with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: +### query.graph -# Tax Region +```ts +const { data: variants } = await query.graph({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) -In this document, you’ll learn about tax regions and how to use them with the Region Module. +// variants.price_set +``` -## What is a Tax Region? +### useQueryGraphStep -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. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -Tax regions can inherit settings and rules from a parent tax region. +// ... -Each tax region has tax rules and a tax provider. +const { data: variants } = useQueryGraphStep({ + entity: "variant", + fields: [ + "price_set.*", + ], +}) +// variants.price_set +``` -# User Module Options +### Manage with Link -In this document, you'll learn about the options of the User Module. +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -## Module Options +### link.create -```ts title="medusa-config.ts" +```ts import { Modules } from "@medusajs/framework/utils" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/user", - options: { - jwt_secret: process.env.JWT_SECRET, - }, - }, - ], +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, }) ``` -|Option|Description|Required| -|---|---|---|---|---| -|\`jwt\_secret\`|A string indicating the secret used to sign the invite tokens.|Yes| +### createRemoteLinkStep -### Environment Variables +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" -Make sure to add the necessary environment variables for the above options in `.env`: +// ... -```bash -JWT_SECRET=supersecret +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) ``` +*** -# User Creation Flows - -In this document, learn the different ways to create a user using the User Module. - -## Straightforward User Creation +## Sales Channel Module -To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): +The Sales Channel Module provides functionalities to manage multiple selling channels in your store. -```ts -const user = await userModuleService.createUsers({ - email: "user@example.com", -}) -``` +Medusa defines a link between the `Product` and `SalesChannel` data models. A product can have different availability in different sales channels. -You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). +![A diagram showcasing an example of how data models from the Product and Sales Channel modules are linked.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651840/Medusa%20Resources/product-sales-channel_t848ik.jpg) -*** +### Retrieve with Query -## Invite Users +To retrieve the sales channels of a product with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: -To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: +### query.graph ```ts -const invite = await userModuleService.createInvites({ - email: "user@example.com", +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "sales_channels.*", + ], }) + +// products.sales_channels ``` -Later, you can accept the invite and create a new user for them: +### useQueryGraphStep ```ts -const invite = - await userModuleService.validateInviteToken("secret_123") +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -await userModuleService.updateInvites({ - id: invite.id, - accepted: true, -}) +// ... -const user = await userModuleService.createUsers({ - email: invite.email, +const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "sales_channels.*", + ], }) -``` - -### Invite Expiry -An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): - -```ts -await userModuleService.refreshInviteTokens(["invite_123"]) +// products.sales_channels ``` -*** - -## Create Identity with the Auth Module +### Manage with Link -By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: +### link.create ```ts -const { success, authIdentity } = - await authModuleService.authenticate("emailpass", { - // ... - }) +import { Modules } from "@medusajs/framework/utils" -const [, count] = await userModuleService.listAndCountUsers({ - email: authIdentity.entity_id, -}) +// ... -if (!count) { - const user = await userModuleService.createUsers({ - email: authIdentity.entity_id, - }) -} +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` +### createRemoteLinkStep -# 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: - -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // options... - }, - }, - ], - }, - }, - ], +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, }) ``` -### 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 -} -\`\`\`| +# Product Variant Inventory -*** +# Product Variant Inventory -## Related Guides +In this guide, you'll learn about the inventory management features related to product variants. -- [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) +## Configure Inventory Management of Product Variants +A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant. -# GitHub Auth Module Provider +The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +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. -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). +## How the Medusa Application Manages Inventory -*** +When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. -## Register the Github Auth Module Provider +![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) -### Prerequisites +The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. -- [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) +![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) -Add the module to the array of providers passed to the Auth Module: +Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. -// ... +![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-github", - id: "github", - options: { - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - callbackUrl: process.env.GITHUB_CALLBACK_URL, - }, - }, - ], - }, - }, - ], -}) -``` +Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). -### Environment Variables +### Product Inventory in Storefronts -Make sure to add the necessary environment variables for the above options in `.env`: +When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. -```plain -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GITHUB_CALLBACK_URL= -``` +The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. -### Module Options +For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. -|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| +![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) *** -## Override Callback URL During Authentication +## Variant Back Orders -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. +Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. -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). +You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). *** -## Examples +## Additional Resources -- [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). +The following guides provide more details on inventory management in the Medusa application: +- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. +- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. +- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). -# Google Auth Module Provider -In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. +# Links between Sales Channel Module and Other Modules -The Google Auth Module Provider authenticates users with their Google account. +This document showcases the module links defined between the Sales Channel Module and other commerce modules. -Learn about the authentication flow for third-party providers in [this guide](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). +## Summary + +The Sales Channel 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. + +- [`ApiKey` data model of the API Key Module \<> `SalesChannel` data model](#api-key-module). +- [`SalesChannel` data model \<> `Cart` data model of the Cart Module](#cart-module). (Read-only) +- [`SalesChannel` data model \<> `Order` data model of the Order Module](#order-module). (Read-only) +- [`Product` data model of the Product Module \<> `SalesChannel` data model](#product-module). +- [`SalesChannel` data model \<> `StockLocation` data model of the Stock Location Module](#stock-location-module). *** -## Register the Google Auth Module Provider +## API Key Module -### Prerequisites +A publishable API key allows you to easily specify the sales channel scope in a client request. -- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) -- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) +Medusa defines a link between the `ApiKey` and the `SalesChannel` data models. -Add the module to the array of providers passed to the Auth Module: +![A diagram showcasing an example of how resources from the Sales Channel and API Key modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709812064/Medusa%20Resources/sales-channel-api-key_zmqi2l.jpg) -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +### Retrieve with Query -// ... +To retrieve the API keys associated with a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `publishable_api_keys.*` in `fields`: -module.exports = defineConfig({ - // ... - modules: [ - { - // ... - [Modules.AUTH]: { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-google", - id: "google", - options: { - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: process.env.GOOGLE_CALLBACK_URL, - }, - }, - ], - }, - }, - }, +### query.graph + +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", ], }) + +// salesChannels.publishable_api_keys ``` -### Environment Variables +### useQueryGraphStep -Make sure to add the necessary environment variables for the above options in `.env`: +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -```plain -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -GOOGLE_CALLBACK_URL= +// ... + +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "publishable_api_keys.*", + ], +}) + +// salesChannels.publishable_api_keys ``` -### Module Options +### Manage with Link -|Configuration|Description|Required| -|---|---|---|---|---| -|\`clientId\`|A string indicating the |Yes| -|\`clientSecret\`|A string indicating the |Yes| -|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| +To manage the sales channels of an API key, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -*** +### link.create -*** +```ts +import { Modules } from "@medusajs/framework/utils" -## Override Callback URL During Authentication +// ... -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. +await link.create({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` -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). +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.API_KEY]: { + api_key_id: "apk_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) +``` *** -## Examples +## Cart Module -- [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). +Medusa defines a read-only link between the `SalesChannel` data model and the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model. This means you can retrieve the details of a sales channel's carts, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. +### Retrieve with Query -# Get Product Variant Prices using Query +To retrieve the carts of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: -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). +### query.graph -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. +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "carts.*", + ], +}) -So, to retrieve data across the linked records of the two modules, you use Query. +// salesChannels.carts +``` -## Retrieve All Product Variant Prices +### useQueryGraphStep -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -For example: +// ... -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "*", - "variants.*", - "variants.prices.*", + "carts.*", ], - 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). +// salesChannels.carts +``` *** -## Retrieve Calculated Price for a Context +## Order Module -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. +Medusa defines a read-only link between the `SalesChannel` data model and the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model. This means you can retrieve the details of a sales channel's orders, 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. -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). +### Retrieve with Query -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: +To retrieve the orders of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: -- 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. +### query.graph -For example: +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "orders.*", + ], +}) -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" +// salesChannels.orders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = await query.graph({ - entity: "product", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "*", - "variants.*", - "variants.calculated_price.*", + "orders.*", ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, }) -``` -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`. +// salesChannels.orders +``` -`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). +*** -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). +## Product Module +A product has different availability for different sales channels. Medusa defines a link between the `Product` and the `SalesChannel` data models. -# Calculate Product Variant Price with Taxes +![A diagram showcasing an example of how resources from the Sales Channel and Product modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1709809833/Medusa%20Resources/product-sales-channel_t848ik.jpg) -In this document, you'll learn how to calculate a product variant's price with taxes. +A product can be available in more than one sales channel. You can retrieve only the products of a sales channel. -## Step 0: Resolve Resources +### Retrieve with Query -You'll need the following resources for the taxes calculation: +To retrieve the products of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: -1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). -2. The Tax Module's main service to get the tax lines for each product. +### query.graph ```ts -// other imports... -import { - Modules, - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "products.*", + ], +}) -// In an API route, workflow step, etc... -const query = container.resolve(ContainerRegistrationKeys.QUERY) -const taxModuleService = container.resolve( - Modules.TAX -) +// salesChannels.products ``` -*** - -## Step 1: Retrieve Prices for a Context - -After resolving the resources, use Query to retrieve the products with the variants' prices for a context: - -Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). +### useQueryGraphStep ```ts -import { QueryContext } from "@medusajs/framework/utils" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: products } = await query.graph({ - entity: "product", +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", fields: [ - "*", - "variants.*", - "variants.calculated_price.*", + "products.*", ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "region_123", - currency_code: "usd", - }), - }, - }, }) + +// salesChannels.products ``` -*** +### Manage with Link -## Step 2: Get Tax Lines for Products +To manage the sales channels of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -To retrieve the tax line of each product, first, add the following utility method: +### link.create ```ts -// other imports... -import { - HttpTypes, - TaxableItemDTO, -} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" // ... -const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { - return product.variants - ?.map((variant) => { - if (!variant.calculated_price) { - return - } - return { - id: variant.id, - product_id: product.id, - product_name: product.title, - product_categories: product.categories?.map((c) => c.name), - product_category_id: product.categories?.[0]?.id, - product_sku: variant.sku, - product_type: product.type, - product_type_id: product.type_id, - quantity: 1, - unit_price: variant.calculated_price.calculated_amount, - currency_code: variant.calculated_price.currency_code, - } - }) - .filter((v) => !!v) as unknown as TaxableItemDTO[] -} +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` -This formats the products as items to calculate tax lines for. - -Then, use it when retrieving the tax lines of the products retrieved earlier: +### createRemoteLinkStep ```ts -// other imports... -import { - ItemTaxLineDTO, -} from "@medusajs/framework/types" +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -const taxLines = (await taxModuleService.getTaxLines( - products.map(asTaxItem).flat(), - { - // example of context properties. You can pass other ones. - address: { - country_code, - }, - } -)) as unknown as ItemTaxLineDTO[] + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, +}) ``` -You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. +*** -For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. +## Stock Location Module -For the second parameter, you pass the current context. You can pass other details such as the customer's ID. +A stock location is associated with a sales channel. This scopes inventory quantities associated with that stock location by the associated sales channel. -Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). +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) -## Step 3: Calculate Price with Tax for Variant +### Retrieve with Query -To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: +To retrieve the stock locations of a sales channel with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: -```ts highlights={taxLineHighlights} -const taxLinesMap = new Map() -taxLines.forEach((taxLine) => { - const variantId = taxLine.line_item_id - if (!taxLinesMap.has(variantId)) { - taxLinesMap.set(variantId, []) - } +### query.graph - taxLinesMap.get(variantId)?.push(taxLine) +```ts +const { data: salesChannels } = await query.graph({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], }) -``` -Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. +// salesChannels.stock_locations +``` -Then, loop over the products and their variants to retrieve the prices with and without taxes: +### useQueryGraphStep -```ts highlights={calculateTaxHighlights} -// other imports... -import { - calculateAmountsWithTax, -} from "@medusajs/framework/utils" +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -products.forEach((product) => { - product.variants?.forEach((variant) => { - if (!variant.calculated_price) { - return - } - const taxLinesForVariant = taxLinesMap.get(variant.id) || [] - const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ - taxLines: taxLinesForVariant, - amount: variant.calculated_price!.calculated_amount!, - includesTax: - variant.calculated_price!.is_calculated_price_tax_inclusive!, - }) - - // do something with prices... - }) +const { data: salesChannels } = useQueryGraphStep({ + entity: "sales_channel", + fields: [ + "stock_locations.*", + ], }) -``` -For each product variant, you: - -1. Retrieve its tax lines from the `taxLinesMap`. -2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. -3. The `calculateAmountsWithTax` function returns an object having two properties: - - `priceWithTax`: The variant's price with the taxes applied. - - `priceWithoutTax`: The variant's price without taxes applied. +// salesChannels.stock_locations +``` +### Manage with Link -# Stripe Module Provider +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. +### link.create -## Register the Stripe Module Provider +```ts +import { Modules } from "@medusajs/framework/utils" -### Prerequisites +// ... -- [Stripe account](https://stripe.com/) -- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) -- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` -The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: +### createRemoteLinkStep -```ts title="medusa-config.ts" +```ts import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, - ], +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, }) ``` -### Environment Variables -Make sure to add the necessary environment variables for the above options in `.env`: +# Publishable API Keys with Sales Channels + +In this document, you’ll learn what publishable API keys are and how to use them with sales channels. + +## Publishable API Keys with Sales Channels + +A publishable API key, provided by the API Key Module, is a client key scoped to one or more sales channels. + +When sending a request to a Store API route, you must pass a publishable API key in the header of the request: ```bash -STRIPE_API_KEY= +curl http://localhost:9000/store/products \ + x-publishable-api-key: {your_publishable_api_key} ``` -### Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| -|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| -|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| -|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| -|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| +The Medusa application infers the associated sales channels and ensures that only data relevant to the sales channel are used. *** -## Setup Stripe Webhooks +## How to Create a Publishable API Key? -For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. +To create a publishable API key, either use the Medusa Admin or the [Admin API Routes](https://docs.medusajs.com/api/admin#publishable-api-keys). -### Webhook URL -Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: +# Stock Location Concepts -- `{server_url}` is the URL to your deployed Medusa application in server mode. -- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. +In this document, you’ll learn about the main concepts in the Stock Location Module. -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: +## 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. + +- [`FulfillmentSet` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). +- [`FulfillmentProvider` data model of the Fulfillment Module \<> `StockLocation` data model](#fulfillment-module). +- [`StockLocation` data model \<> `Inventory` data model of the Inventory Module](#inventory-module). +- [`SalesChannel` data model of the Sales Channel Module \<> `StockLocation` data model](#sales-channel-module). + +*** + +## 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.fulfillment_sets +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "fulfillment_sets.*", + ], +}) + +// stockLocations.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 `StockLocation` data model and the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model. This means you can retrieve the details of a stock location's inventory levels, 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 inventory levels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `inventory_levels.*` in `fields`: + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "inventory_levels.*", + ], +}) + +// stockLocations.inventory_levels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "inventory_levels.*", + ], +}) + +// stockLocations.inventory_levels +``` + +*** + +## 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.sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations.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. + +- [`Currency` data model \<> `Currency` data model of Currency Module](#currency-module). (Read-only). + +*** + +## 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 `Currency` 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` data model in the Store 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.supported_currencies +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores.supported_currencies +``` + + +# 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 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 Rates and Rules + +In this document, you’ll learn about tax rates and rules. + +## 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. + +## 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. + + +# User Creation Flows + +In this document, learn the different ways to create a user using the User Module. + +## Straightforward User Creation + +To create a user, use the [create method of the User Module’s main service](https://docs.medusajs.com/references/user/create/index.html.md): + +```ts +const user = await userModuleService.createUsers({ + email: "user@example.com", +}) +``` + +You can pair this with the Auth Module to allow the user to authenticate, as explained in a [later section](#create-identity-with-the-auth-module). + +*** + +## Invite Users + +To create a user, you can create an invite for them using the [createInvites method](https://docs.medusajs.com/references/user/createInvites/index.html.md) of the User Module's main service: + +```ts +const invite = await userModuleService.createInvites({ + email: "user@example.com", +}) +``` + +Later, you can accept the invite and create a new user for them: + +```ts +const invite = + await userModuleService.validateInviteToken("secret_123") + +await userModuleService.updateInvites({ + id: invite.id, + accepted: true, +}) + +const user = await userModuleService.createUsers({ + email: invite.email, +}) +``` + +### Invite Expiry + +An invite has an expiry date. You can renew the expiry date and refresh the token using the [refreshInviteTokens method](https://docs.medusajs.com/references/user/refreshInviteTokens/index.html.md): + +```ts +await userModuleService.refreshInviteTokens(["invite_123"]) +``` + +*** + +## Create Identity with the Auth Module + +By combining the User and Auth Modules, you can use the Auth Module for authenticating users, and the User Module to manage those users. + +So, when a user is authenticated, and you receive the `AuthIdentity` object, you can use it to create a user if it doesn’t exist: + +```ts +const { success, authIdentity } = + await authModuleService.authenticate("emailpass", { + // ... + }) + +const [, count] = await userModuleService.listAndCountUsers({ + email: authIdentity.entity_id, +}) + +if (!count) { + const user = await userModuleService.createUsers({ + email: authIdentity.entity_id, + }) +} +``` + + +# 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 +``` + + +# 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: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // options... + }, + }, + ], + }, + }, + ], +}) +``` + +### 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 +} +\`\`\`| + +*** + +## Related Guides + +- [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) + + +# Google Auth Module Provider + +In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. + +The Google Auth Module Provider authenticates users with their Google account. + +Learn about the authentication flow for third-party providers in [this guide](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). + +*** + +## Register the Google Auth Module Provider + +### Prerequisites + +- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) +- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) + +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" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + // ... + [Modules.AUTH]: { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-google", + id: "google", + options: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackUrl: process.env.GOOGLE_CALLBACK_URL, + }, + }, + ], + }, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL= +``` + +### Module Options + +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the |Yes| +|\`clientSecret\`|A string indicating the |Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| + +*** + +*** + +## Override Callback URL During Authentication + +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 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 + +In this document, you’ll learn about the GitHub 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). + +*** + +## Register the Github Auth Module Provider + +### Prerequisites + +- [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: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-github", + id: "github", + options: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GITHUB_CALLBACK_URL, + }, + }, + ], + }, + }, + ], +}) +``` + +### 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| + +*** + +## Override Callback URL During Authentication + +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). + + +# Stripe Module Provider + +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. + +## Register the Stripe Module Provider + +### Prerequisites + +- [Stripe account](https://stripe.com/) +- [Stripe Secret API Key](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) +- [For deployed Medusa applications, a Stripe webhook secret. Refer to the end of this guide for details on the URL and events.](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) + +The Stripe Module Provider is installed by default in your application. To use it, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: + +```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: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```bash +STRIPE_API_KEY= +``` + +### Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`apiKey\`|A string indicating the Stripe Secret API key.|Yes|-| +|\`webhookSecret\`|A string indicating the Stripe webhook secret. This is only useful for deployed Medusa applications.|Yes|-| +|\`capture\`|Whether to automatically capture payment after authorization.|No|\`false\`| +|\`automatic\_payment\_methods\`|A boolean value indicating whether to enable Stripe's automatic payment methods. This is useful if you integrate services like Apple pay or Google pay.|No|\`false\`| +|\`payment\_description\`|A string used as the default description of a payment if none is available in cart.context.payment\_description.|No|-| + +*** + +## Setup Stripe Webhooks + +For production applications, you must set up webhooks in Stripe that inform Medusa of changes and updates to payments. Refer to [Stripe's documentation](https://docs.stripe.com/webhooks#add-a-webhook-endpoint) on how to setup webhooks. + +### Webhook URL + +Medusa has a `{server_url}/hooks/payment/{provider_id}` API route that you can use to register webhooks in Stripe, where: + +- `{server_url}` is the URL to your deployed Medusa application in server mode. +- `{provider_id}` is the ID of the provider, such as `stripe_stripe` for basic payments. + +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: |Stripe Payment Type|Webhook Endpoint URL| |---|---|---| @@ -24728,598 +24463,862 @@ The Stripe Module Provider supports the following payment types, and the webhook |Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| |PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| -### Webhook Events +### Webhook Events + +When you set up the webhook in Stripe, choose the following events to listen to: + +- `payment_intent.amount_capturable_updated` +- `payment_intent.succeeded` +- `payment_intent.payment_failed` + +*** + +## Useful Guides + +- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). +- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). +- [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 + +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). + +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. + +So, to retrieve data across the linked records of the two modules, you use Query. + +## Retrieve All Product Variant Prices + +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. + +For example: + +```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). + +*** + +## 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", + }), + }, + }, +}) +``` + +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`. + +`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). + +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). + + +# Calculate Product Variant Price with Taxes + +In this document, you'll learn how to calculate a product variant's price with taxes. + +## Step 0: Resolve Resources + +You'll need the following resources for the taxes calculation: + +1. [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). +2. The Tax Module's main service to get the tax lines for each product. + +```ts +// other imports... +import { + Modules, + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +// In an API route, workflow step, etc... +const query = container.resolve(ContainerRegistrationKeys.QUERY) +const taxModuleService = container.resolve( + Modules.TAX +) +``` + +*** + +## Step 1: Retrieve Prices for a Context + +After resolving the resources, use Query to retrieve the products with the variants' prices for a context: + +Learn more about retrieving product variants' prices for a context in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/price/index.html.md). + +```ts +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: "region_123", + currency_code: "usd", + }), + }, + }, +}) +``` + +*** + +## Step 2: Get Tax Lines for Products -When you set up the webhook in Stripe, choose the following events to listen to: +To retrieve the tax line of each product, first, add the following utility method: -- `payment_intent.amount_capturable_updated` -- `payment_intent.succeeded` -- `payment_intent.payment_failed` +```ts +// other imports... +import { + HttpTypes, + TaxableItemDTO, +} from "@medusajs/framework/types" + +// ... +const asTaxItem = (product: HttpTypes.StoreProduct): TaxableItemDTO[] => { + return product.variants + ?.map((variant) => { + if (!variant.calculated_price) { + return + } + + return { + id: variant.id, + product_id: product.id, + product_name: product.title, + product_categories: product.categories?.map((c) => c.name), + product_category_id: product.categories?.[0]?.id, + product_sku: variant.sku, + product_type: product.type, + product_type_id: product.type_id, + quantity: 1, + unit_price: variant.calculated_price.calculated_amount, + currency_code: variant.calculated_price.currency_code, + } + }) + .filter((v) => !!v) as unknown as TaxableItemDTO[] +} +``` + +This formats the products as items to calculate tax lines for. + +Then, use it when retrieving the tax lines of the products retrieved earlier: + +```ts +// other imports... +import { + ItemTaxLineDTO, +} from "@medusajs/framework/types" + +// ... +const taxLines = (await taxModuleService.getTaxLines( + products.map(asTaxItem).flat(), + { + // example of context properties. You can pass other ones. + address: { + country_code, + }, + } +)) as unknown as ItemTaxLineDTO[] +``` + +You use the Tax Module's main service's [getTaxLines method](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md) to retrieve the tax line. + +For the first parameter, you use the `asTaxItem` function to format the products as expected by the `getTaxLines` method. + +For the second parameter, you pass the current context. You can pass other details such as the customer's ID. + +Learn about the other context properties to pass in [the getTaxLines method's reference](https://docs.medusajs.com/references/tax/getTaxLines/index.html.md). *** -## Useful Guides +## Step 3: Calculate Price with Tax for Variant -- [Storefront guide: Add Stripe payment method during checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/payment/stripe/index.html.md). -- [Integrate in Next.js Starter](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter#stripe-integration/index.html.md). -- [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). +To calculate the price with and without taxes for a variant, first, group the tax lines retrieved in the previous step by variant IDs: + +```ts highlights={taxLineHighlights} +const taxLinesMap = new Map() +taxLines.forEach((taxLine) => { + const variantId = taxLine.line_item_id + if (!taxLinesMap.has(variantId)) { + taxLinesMap.set(variantId, []) + } + + taxLinesMap.get(variantId)?.push(taxLine) +}) +``` + +Notice that the variant's ID is stored in the `line_item_id` property of a tax line since tax lines are used for line items in a cart. + +Then, loop over the products and their variants to retrieve the prices with and without taxes: + +```ts highlights={calculateTaxHighlights} +// other imports... +import { + calculateAmountsWithTax, +} from "@medusajs/framework/utils" + +// ... +products.forEach((product) => { + product.variants?.forEach((variant) => { + if (!variant.calculated_price) { + return + } + + const taxLinesForVariant = taxLinesMap.get(variant.id) || [] + const { priceWithTax, priceWithoutTax } = calculateAmountsWithTax({ + taxLines: taxLinesForVariant, + amount: variant.calculated_price!.calculated_amount!, + includesTax: + variant.calculated_price!.is_calculated_price_tax_inclusive!, + }) + + // do something with prices... + }) +}) +``` + +For each product variant, you: + +1. Retrieve its tax lines from the `taxLinesMap`. +2. Calculate its prices with and without taxes using the `calculateAmountsWithTax` from the Medusa Framework. +3. The `calculateAmountsWithTax` function returns an object having two properties: + - `priceWithTax`: The variant's price with the taxes applied. + - `priceWithoutTax`: The variant's price without taxes applied. ## Workflows - [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/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) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) - [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) +- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/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) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) - [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) +- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) - [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) -- [updateCartPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartPromotionsWorkflow/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) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/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) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/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) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/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) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) +- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/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) - [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/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) +- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) +- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/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) +- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/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) - [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) - [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/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) - [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) -- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md) - [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/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) -- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) +- [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/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) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/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) - [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) -- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/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) - [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [updateInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryItemsWorkflow/index.html.md) - [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) - [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/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) -- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/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) - [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) - [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) - [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [updatePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListsWorkflow/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) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/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) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/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) - [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) - [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/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) - [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) +- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/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) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/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) - [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) - [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) -- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) - [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) +- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) - [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/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) - [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) - [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) - [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) - [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/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) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) - [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) -- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) +- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/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) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) -- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) - [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/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) - [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) -- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/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) +- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/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) - [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) - [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) - [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) -- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/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) +- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/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) - [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) -- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) +- [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) +- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) - [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/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) -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/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) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) - [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/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) - [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) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) - [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) - [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) - [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) - [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/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) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/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) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) - [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/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) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/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) - [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/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) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/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) - [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/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) -- [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) - [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/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) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) - [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/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) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/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) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/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) -- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/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) - [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/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) - [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) - [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/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) +- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/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) - [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/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) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/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) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/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) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/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) - [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) - [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) - [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) +- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/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) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) -- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/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) +- [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) +- [batchProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductVariantsWorkflow/index.html.md) +- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) - [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) +- [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/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) - [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/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) - [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/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) +- [deleteProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductsWorkflow/index.html.md) - [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/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) +- [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/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) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/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) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/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) - [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) -- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/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) - [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) - [createPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionRulesWorkflow/index.html.md) - [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) +- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) -- [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/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) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/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) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/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) -- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) - [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/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) +- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) - [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) - [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) - [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/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) +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) - [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/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) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) -- [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/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) +- [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) - [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/index.html.md) - [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) +- [createUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUsersWorkflow/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) -- [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/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) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) - [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) - [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) - [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) -- [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) - [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/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) ## Steps +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/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) -- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) +- [validateSalesChannelsExistStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateSalesChannelsExistStep/index.html.md) +- [updateApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateApiKeysStep/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) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) - [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) -- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) - [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/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) -- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) -- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) -- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) -- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/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) -- [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) -- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/index.html.md) -- [validateCustomerAccountCreation](https://docs.medusajs.com/references/medusa-workflows/steps/validateCustomerAccountCreation/index.html.md) -- [updateCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomersStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/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) - [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) - [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) - [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/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) - [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/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) - [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) - [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/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) -- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/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) +- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/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) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) - [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/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) - [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) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) - [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) +- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/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) +- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/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) +- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/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) -- [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) -- [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) -- [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/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) -- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/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) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [createCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerGroupsStep/index.html.md) +- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) +- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) +- [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) +- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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) -- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) -- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/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) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) - [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) - [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) - [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) +- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/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) -- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/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) +- [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) - [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/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) -- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) -- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/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) +- [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) +- [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) +- [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/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) -- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) - [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/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) +- [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) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/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) - [addOrderTransactionStep](https://docs.medusajs.com/references/medusa-workflows/steps/addOrderTransactionStep/index.html.md) - [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/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) -- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) - [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/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) +- [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) - [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) +- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) - [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/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) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/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) - [createOrderLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderLineItemsStep/index.html.md) - [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/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) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/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) - [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/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) -- [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) -- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) +- [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/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) - [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) -- [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) - [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) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/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) +- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) +- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/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) - [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/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) -- [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) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/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) -- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/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) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/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) +- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/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) - [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/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) -- [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) - [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/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) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/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) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) - [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) - [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/index.html.md) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) - [createProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTagsStep/index.html.md) -- [createProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductTypesStep/index.html.md) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/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) -- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) -- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/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) +- [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) -- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/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) - [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) - [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/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) -- [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) - [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) - [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) -- [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/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) - [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) -- [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) -- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/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) -- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) +- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) +- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/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) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/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) +- [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) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) +- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) +- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) -- [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) - [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) - [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/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) +- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/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) -- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) -- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) -- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/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) -- [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/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) - [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) -- [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) +- [updateSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateSalesChannelsStep/index.html.md) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/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) +- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) +- [deleteReturnReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnReasonStep/index.html.md) +- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/index.html.md) - [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/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) @@ -25331,13 +25330,14 @@ When you set up the webhook in Stripe, choose the following events to listen to: - [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) - [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) - [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) -- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) - [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/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) +- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) +- [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) # Medusa CLI Reference @@ -25454,6 +25454,22 @@ medusa new [ []] |\`--db-host \\`|The database host to use for database setup.| +# 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| + + # 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. @@ -25590,22 +25606,6 @@ 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.| -# 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| - - # 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. @@ -25667,6 +25667,22 @@ npx medusa plugin:build ``` +# 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\`| + + # start-cluster Command - Medusa CLI Reference Starts the Medusa application in [cluster mode](https://expressjs.com/en/advanced/best-practice-performance.html#run-your-app-in-a-cluster). @@ -25702,22 +25718,6 @@ npx medusa telemetry |\`--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\`| - - # user Command - Medusa CLI Reference Create a new admin user. @@ -25942,22 +25942,6 @@ 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.| -# 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\`| - - # 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. @@ -25987,38 +25971,6 @@ medusa new [ []] |\`--db-host \\`|The database host to use for database setup.| -# 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. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - # 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. @@ -26080,23 +26032,36 @@ npx medusa plugin:build ``` -# user Command - Medusa CLI Reference +# exec Command - Medusa CLI Reference -Create a new admin user. +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 user --email [--password ] +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. + +```bash +npx medusa start ``` ## Options -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`-e \\`|The user's email.|Yes|-| -|\`-p \\`|The user's password.|No|-| -|\`-i \\`|The user's ID.|No|An automatically generated ID.| -|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. -If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| # start-cluster Command - Medusa CLI Reference @@ -26118,6 +26083,22 @@ npx medusa start-cluster |\`-p \\`|Set port of the Medusa server.|\`9000\`| +# 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\`| + + # 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. @@ -26126,12 +26107,31 @@ Enable or disable the collection of anonymous data usage. If no option is provid npx medusa telemetry ``` -#### Options +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + +# user Command - Medusa CLI Reference + +Create a new admin user. + +```bash +npx medusa user --email [--password ] +``` + +## Options -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`-e \\`|The user's email.|Yes|-| +|\`-p \\`|The user's password.|No|-| +|\`-i \\`|The user's ID.|No|An automatically generated ID.| +|\`--invite\`|Whether to create an invite instead of a user. When using this option, you don't need to specify a password. +If ran successfully, you'll receive the invite token in the output.|No|\`false\`| # Medusa JS SDK @@ -26398,1461 +26398,984 @@ Learn more in the [Next.js documentation](https://nextjs.org/docs/app/building-y - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) - [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/index.html.md) -- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) -- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) -- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) -- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) -- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) -- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) -- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) -- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) +- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) - [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) - [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) - [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) - [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) +- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) +- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) +- [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) +- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) +- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) +- [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) +- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) +- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/index.html.md) +- [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/index.html.md) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) +- [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) - [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) - [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/index.html.md) - [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) - [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) - [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) +- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) - [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) - [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) - [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) -- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) +- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) +- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) - [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) +- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) - [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) - [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) -- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) -- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.delete/index.html.md) -- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) -- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) - [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) - [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) - [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) -- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) - [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/index.html.md) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) +- [markAsDelivered](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.markAsDelivered/index.html.md) - [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) +- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) -- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) -- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) +- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) - [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) - [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) - [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) - [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) +- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) +- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) - [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) - [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) -- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) - [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) - [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) - [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [batchVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariants/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) - [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) - [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) - [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) +- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/index.html.md) -- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) - [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) - [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) - [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) - [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) - [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) -- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) - [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.update/index.html.md) +- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) - [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.update/index.html.md) - [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) -- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) - [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) +- [cancelReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelReceive/index.html.md) - [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancelRequest/index.html.md) - [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) +- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) - [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) - [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/index.html.md) -- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) +- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) +- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) - [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) - [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) - [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) - [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) - [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) - [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) +- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) +- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) +- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) +- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) -- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) - - -## JS SDK Auth - -- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) -- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) -- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) -- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) -- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) -- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) - - -## JS SDK Store - -- [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) -- [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) -- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) -- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) -- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) -- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) - - -# Configure Medusa Backend - -In this document, you’ll learn how to create a file service in the Medusa application and the methods you must implement in it. - -The configurations for your Medusa application are in `medusa-config.ts` located in the root of your Medusa project. The configurations include configurations for database, modules, and more. - -`medusa-config.ts` exports the value returned by the `defineConfig` utility function imported from `@medusajs/framework/utils`. - -`defineConfig` accepts as a parameter an object with the following properties: - -- [projectConfig](https://docs.medusajs.com/references/medusa-config#projectconfig/index.html.md) (required): An object that holds general configurations related to the Medusa application, such as database or CORS configurations. -- [plugins](https://docs.medusajs.com/references/medusa-config#plugins/index.html.md): An array of strings or objects that hold the configurations of the plugins installed in the Medusa application. -- [admin](https://docs.medusajs.com/references/medusa-config#admin/index.html.md): An object that holds admin-related configurations. -- [modules](https://docs.medusajs.com/references/medusa-config#modules/index.html.md): An object that configures the Medusa application's modules. -- [featureFlags](https://docs.medusajs.com/references/medusa-config#featureflags/index.html.md): An object that enables or disables features guarded by a feature flag. - -For example: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - }, - admin: { - // ... - }, - modules: { - // ... - }, - featureFlags: { - // ... - } -}) -``` - -*** - -## Environment Variables - -It's highly recommended to store the values of configurations in environment variables, then reference them within `medusa-config.ts`. - -During development, you can set your environment variables in the `.env` file at the root of your Medusa application project. In production, -setting the environment variables depends on the hosting provider. - -*** - -## projectConfig - -This property holds essential configurations related to the Medusa application, such as database and CORS configurations. - -### databaseName - -The name of the database to connect to. If the name is specified in `databaseUrl`, then you don't have to use this configuration. - -Make sure to create the PostgreSQL database before using it. You can check how to create a database in -[PostgreSQL's documentation](https://www.postgresql.org/docs/current/sql-createdatabase.html). - -#### Example - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseName: process.env.DATABASE_NAME || - "medusa-store", - // ... - }, - // ... -}) -``` - -### databaseUrl - -The PostgreSQL connection URL of the database, which is of the following format: - -```bash -postgres://[user][:password]@[host][:port]/[dbname] -``` - -Where: - -- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html). -- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password. -- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`. -- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port. -- `[dbname]`: (required) the name of the database. - -You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html). - -#### Example - -For example, set the following database URL in your environment variables: - -```bash -DATABASE_URL=postgres://postgres@localhost/medusa-store -``` - -Then, use the value in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseUrl: process.env.DATABASE_URL, - // ... - }, - // ... -}) -``` - -### databaseSchema - -The database schema to connect to. This is not required to provide if you’re using the default schema, which is `public`. - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseSchema: process.env.DATABASE_SCHEMA || - "custom", - // ... - }, - // ... -}) -``` - -### databaseLogging - -This configuration specifies whether database messages should be logged. - -#### Example - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseLogging: false - // ... - }, - // ... -}) -``` - -### databaseDriverOptions - -This configuration is used to pass additional options to the database connection. You can pass any configuration. For example, pass the -`ssl` property that enables support for TLS/SSL connections. - -This is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. -During development, it’s recommended not to pass this option. - -:::note - -Make sure to add to the end of the database URL `?ssl_mode=disable` as well when disabling `rejectUnauthorized`. - -::: - -#### Example - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - databaseDriverOptions: process.env.NODE_ENV !== "development" ? - { connection: { ssl: { rejectUnauthorized: false } } } : {} - // ... - }, - // ... -}) -``` - -#### Properties - -- connection: (\`object\`) - - - ssl: (\`object\`) Configure support for TLS/SSL connection - - - rejectUnauthorized: (\`false\`) Whether to fail connection if the server certificate is verified against the list of supplied CAs and the hostname and no match is found. - -### redisUrl - -This configuration specifies the connection URL to Redis to store the Medusa server's session. - -:::note - -You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/). - -::: - -The Redis connection URL has the following format: - -```bash -redis[s]://[[username][:password]@][host][:port][/db-number] -``` - -For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation. +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) -#### Example -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisUrl: process.env.REDIS_URL || - "redis://localhost:6379", - // ... - }, - // ... -}) -``` +## JS SDK Auth -### redisPrefix +- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) +- [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) +- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) +- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) +- [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) +- [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) +- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) -This configuration defines a prefix on all keys stored in Redis for the Medusa server's session. The default value is `sess:`. -If this configuration option is provided, it is prepended to `sess:`. +## JS SDK Store -#### Example +- [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md) +- [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) +- [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) +- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) +- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) +- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) +- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) +- [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) +- [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisPrefix: process.env.REDIS_URL || "medusa:", - // ... - }, - // ... -}) -``` -### redisOptions +# Configure Medusa Backend -This configuration defines options to pass ioredis for the Redis connection used to store the Medusa server's session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions) -for the list of available options. +In this document, you’ll learn how to create a file service in the Medusa application and the methods you must implement in it. -#### Example +The configurations for your Medusa application are in `medusa-config.ts` located in the root of your Medusa project. The configurations include configurations for database, modules, and more. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - redisOptions: { - connectionName: process.env.REDIS_CONNECTION_NAME || - "medusa", - } - // ... - }, - // ... -}) -``` +`medusa-config.ts` exports the value returned by the `defineConfig` utility function imported from `@medusajs/framework/utils`. -### sessionOptions +`defineConfig` accepts as a parameter an object with the following properties: -This configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server's session. +- [projectConfig](https://docs.medusajs.com/references/medusa-config#projectconfig/index.html.md) (required): An object that holds general configurations related to the Medusa application, such as database or CORS configurations. +- [plugins](https://docs.medusajs.com/references/medusa-config#plugins/index.html.md): An array of strings or objects that hold the configurations of the plugins installed in the Medusa application. +- [admin](https://docs.medusajs.com/references/medusa-config#admin/index.html.md): An object that holds admin-related configurations. +- [modules](https://docs.medusajs.com/references/medusa-config#modules/index.html.md): An object that configures the Medusa application's modules. +- [featureFlags](https://docs.medusajs.com/references/medusa-config#featureflags/index.html.md): An object that enables or disables features guarded by a feature flag. -#### Example +For example: ```ts title="medusa-config.ts" module.exports = defineConfig({ projectConfig: { - sessionOptions: { - name: process.env.SESSION_NAME || "custom", - } // ... }, - // ... -}) -``` - -#### Properties - -- name: (\`string\`) The name of the session ID cookie to set in the response (and read from in the request). The default value is \`connect.sid\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#name) for more details. -- resave: (\`boolean\`) Whether the session should be saved back to the session store, even if the session was never modified during the request. The default value is \`true\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#resave) for more details. -- rolling: (\`boolean\`) Whether the session identifier cookie should be force-set on every response. The default value is \`false\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#rolling) for more details. -- saveUninitialized: (\`boolean\`) Whether a session that is "uninitialized" is forced to be saved to the store. The default value is \`true\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#saveUninitialized) for more details. -- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \`http.cookieSecret\` is used. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details. -- ttl: (\`number\`) Used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. By default, its value is \`10 \* 60 \* 60 \* 1000\`. - Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookiemaxage) for details. - -### workerMode - -Configure the application's worker mode. - -Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products. - -With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application. - -![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) - -Medusa has three runtime modes: - -- Use `shared` to run the application in a single process. -- Use `worker` to run the a worker process only. -- Use `server` to run the application server only. - -In production, it's recommended to deploy two instances: - -1. One having the `workerMode` configuration set to `server`. -2. Another having the `workerMode` configuration set to `worker`. - -#### Example - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared" + admin: { // ... }, - // ... -}) -``` - -### http - -This property configures the application's http-specific settings. - -#### Example - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - http: { - cookieSecret: "supersecret", - compression: { - // ... - } - } + modules: { // ... }, - // ... + featureFlags: { + // ... + } }) ``` -#### Properties - -- authCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - - \`cors\` is a string used to specify the accepted URLs or patterns for API Routes starting with \`/auth\`. It can either be one accepted origin, or a comma-separated list of accepted origins. - - Every origin in that list must either be: - - 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that Medusa tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- storeCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - - \`store\_cors\` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. - - Every origin in that list must either be: - - 1\. A URL. For example, \`http://localhost:8000\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- adminCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - - \`admin\_cors\` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. - - Every origin in that list must either be: - - 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; - 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. -- jwtSecret: (\`string\`) A random string used to create authentication tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. - - In a development environment, if this option is not set the default secret is \`supersecret\`. However, in production, if this configuration is not set, an - error is thrown and the application crashes. -- jwtExpiresIn: (\`string\`) The expiration time for the JWT token. Its format is based off the \[ms package]\(https://github.com/vercel/ms). - - If not provided, the default value is \`24h\`. -- cookieSecret: (\`string\`) A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. - - In a development environment, if this option is not set, the default secret is \`supersecret\`. However, in production, if this configuration is not set, an error is thrown and - the application crashes. -- compression: (\[HttpCompressionOptions]\(../medusa\_config.HttpCompressionOptions/page.mdx)) Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. - However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. - - If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header \`"x-no-compression": true\`. - Learn more in the \[API Reference]\(https://docs.medusajs.com/api/store#http-compression). - - - enabled: (\`boolean\`) Whether HTTP compression is enabled. By default, it's \`false\`. - - - level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. - A lower level will result in less compression but will be much faster. The default value is \`6\`. - - - memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It's an integer in the range of 1 (minimum level) and 9 (maximum level). - The default value is \`8\`. +*** - - threshold: (\`string\` \\| \`number\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the - \[bytes]\(https://www.npmjs.com/package/bytes) module. The default value is \`1024\`. -- authMethodsPerActor: (\`Record\\`) This configuration specifies the supported authentication providers per actor type (such as \`user\`, \`customer\`, or any custom actors). - For example, you only want to allow SSO logins for \`users\`, while you want to allow email/password logins for \`customers\` to the storefront. +## Environment Variables - \`authMethodsPerActor\` is a a map where the actor type (eg. 'user') is the key, and the value is an array of supported auth provider IDs. -- restrictedFields: (\`object\`) Specifies the fields that can't be selected in the response unless specified in the allowed query config. - This is useful to restrict sensitive fields from being exposed in the API. +It's highly recommended to store the values of configurations in environment variables, then reference them within `medusa-config.ts`. - - store: (\`string\`\[]) +During development, you can set your environment variables in the `.env` file at the root of your Medusa application project. In production, +setting the environment variables depends on the hosting provider. *** -## admin - -This property holds configurations for the Medusa Admin dashboard. +## projectConfig -### Example +This property holds essential configurations related to the Medusa application, such as database and CORS configurations. -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - backendUrl: process.env.MEDUSA_BACKEND_URL || - "http://localhost:9000" - }, - // ... -}) -``` +### databaseName -### disable +The name of the database to connect to. If the name is specified in `databaseUrl`, then you don't have to use this configuration. -Whether to disable the admin dashboard. If set to `true`, the admin dashboard is disabled, -in both development and production environments. The default value is `false`. +Make sure to create the PostgreSQL database before using it. You can check how to create a database in +[PostgreSQL's documentation](https://www.postgresql.org/docs/current/sql-createdatabase.html). #### Example ```ts title="medusa-config.ts" module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false + projectConfig: { + databaseName: process.env.DATABASE_NAME || + "medusa-store", + // ... }, // ... }) ``` -### path - -The path to the admin dashboard. The default value is `/app`. +### databaseUrl -The value cannot be one of the reserved paths: +The PostgreSQL connection URL of the database, which is of the following format: -- `/admin` -- `/store` -- `/auth` -- `/` +```bash +postgres://[user][:password]@[host][:port]/[dbname] +``` -:::note +Where: -When using Docker, make sure that the root path of the Docker image doesn't path the admin's `path`. For example, if the Docker image's root path is `/app`, change -the value of the `path` configuration, as it's `/app` by default. +- `[user]`: (required) your PostgreSQL username. If not specified, the system's username is used by default. The database user that you use must have create privileges. If you're using the `postgres` superuser, then it should have these privileges by default. Otherwise, make sure to grant your user create privileges. You can learn how to do that in [PostgreSQL's documentation](https://www.postgresql.org/docs/current/ddl-priv.html). +- `[:password]`: an optional password for the user. When provided, make sure to put `:` before the password. +- `[host]`: (required) your PostgreSQL host. When run locally, it should be `localhost`. +- `[:port]`: an optional port that the PostgreSQL server is listening on. By default, it's `5432`. When provided, make sure to put `:` before the port. +- `[dbname]`: (required) the name of the database. -::: +You can learn more about the connection URL format in [PostgreSQL’s documentation](https://www.postgresql.org/docs/current/libpq-connect.html). #### Example +For example, set the following database URL in your environment variables: + +```bash +DATABASE_URL=postgres://postgres@localhost/medusa-store +``` + +Then, use the value in `medusa-config.ts`: + ```ts title="medusa-config.ts" module.exports = defineConfig({ - admin: { - path: process.env.ADMIN_PATH || `/app`, + projectConfig: { + databaseUrl: process.env.DATABASE_URL, + // ... }, // ... }) ``` -### outDir - -The directory where the admin build is outputted when you run the `build` command. -The default value is `./build`. +### databaseSchema -#### Example +The database schema to connect to. This is not required to provide if you’re using the default schema, which is `public`. ```ts title="medusa-config.ts" module.exports = defineConfig({ - admin: { - outDir: process.env.ADMIN_BUILD_DIR || `./build`, + projectConfig: { + databaseSchema: process.env.DATABASE_SCHEMA || + "custom", + // ... }, // ... }) ``` -### backendUrl +### databaseLogging -The URL of your Medusa application. Defaults to the browser origin. This is useful to set when running the admin on a separate domain. +This configuration specifies whether database messages should be logged. #### Example ```ts title="medusa-config.ts" module.exports = defineConfig({ - admin: { - backendUrl: process.env.MEDUSA_BACKEND_URL || - "http://localhost:9000" + projectConfig: { + databaseLogging: false + // ... }, // ... }) ``` -### vite - -Configure the Vite configuration for the admin dashboard. This function receives the default Vite configuration -and returns the modified configuration. The default value is `undefined`. - -*** - -## plugins +### databaseDriverOptions -On your Medusa server, you can use [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) to add re-usable Medusa customizations. Plugins -can include modules, workflows, API Routes, and other customizations. Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). +This configuration is used to pass additional options to the database connection. You can pass any configuration. For example, pass the +`ssl` property that enables support for TLS/SSL connections. -Aside from installing the plugin with NPM, you need to pass the plugin you installed into the `plugins` array defined in `medusa-config.ts`. +This is useful for production databases, which can be supported by setting the `rejectUnauthorized` attribute of `ssl` object to `false`. +During development, it’s recommended not to pass this option. -The items in the array can either be: +:::note -- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. You can pass a plugin as a string if it doesn’t require any options. -- An object having the following properties: - - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file. - - `options`: An object that includes options to be passed to the modules within the plugin. Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). +Make sure to add to the end of the database URL `?ssl_mode=disable` as well when disabling `rejectUnauthorized`. -Learn how to create a plugin in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md). +::: -### Example +#### Example ```ts title="medusa-config.ts" -module.exports = { - plugins: [ - `medusa-my-plugin-1`, - { - resolve: `medusa-my-plugin`, - options: { - apiKey: process.env.MY_API_KEY || - `test`, - }, - }, +module.exports = defineConfig({ + projectConfig: { + databaseDriverOptions: process.env.NODE_ENV !== "development" ? + { connection: { ssl: { rejectUnauthorized: false } } } : {} // ... - ], + }, // ... -} +}) ``` -### resolve - -The name of the plugin's package as specified in the plugin's `package.json` file. +#### Properties -### options +- connection: (\`object\`) -An object that includes options to be passed to the modules within the plugin. -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). + - ssl: (\`object\`) Configure support for TLS/SSL connection -*** + - rejectUnauthorized: (\`false\`) Whether to fail connection if the server certificate is verified against the list of supplied CAs and the hostname and no match is found. -## modules +### redisUrl -This property holds all custom modules installed in your Medusa application. +This configuration specifies the connection URL to Redis to store the Medusa server's session. :::note -Medusa's commerce modules are configured by default, so only -add them to this property if you're changing their configurations or adding providers to a module. +You must first have Redis installed. You can refer to [Redis's installation guide](https://redis.io/docs/getting-started/installation/). ::: -`modules` is an array of objects, each holding a module's registration configurations. Each object has the following properties: +The Redis connection URL has the following format: -1. `resolve`: a string indicating the path to the module relative to `src`, or the module's NPM package name. For example, `./modules/my-module`. -2. `options`: (optional) an object indicating the options to pass to the module. +```bash +redis[s]://[[username][:password]@][host][:port][/db-number] +``` -### Example +For a local Redis installation, the connection URL should be `redis://localhost:6379` unless you’ve made any changes to the Redis configuration during installation. + +#### Example ```ts title="medusa-config.ts" module.exports = defineConfig({ - modules: [ - { - resolve: "./modules/hello" - } - ] + projectConfig: { + redisUrl: process.env.REDIS_URL || + "redis://localhost:6379", + // ... + }, // ... }) ``` -*** +### redisPrefix -## featureFlags +This configuration defines a prefix on all keys stored in Redis for the Medusa server's session. The default value is `sess:`. -Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. +If this configuration option is provided, it is prepended to `sess:`. -You can enable a feature in your application by enabling its feature flag. Feature flags are enabled through either environment -variables or through this configuration property exported in `medusa-config.ts`. +#### Example -The `featureFlags`'s value is an object. Its properties are the names of the feature flags, and their value is a boolean indicating whether the feature flag is enabled. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + redisPrefix: process.env.REDIS_URL || "medusa:", + // ... + }, + // ... +}) +``` -You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). +### redisOptions -### Example +This configuration defines options to pass ioredis for the Redis connection used to store the Medusa server's session. Refer to [ioredis’s RedisOptions documentation](https://redis.github.io/ioredis/index.html#RedisOptions) +for the list of available options. + +#### Example ```ts title="medusa-config.ts" module.exports = defineConfig({ - featureFlags: { - analytics: true, + projectConfig: { + redisOptions: { + connectionName: process.env.REDIS_CONNECTION_NAME || + "medusa", + } // ... - } + }, // ... }) ``` -:::note +### sessionOptions -After enabling a feature flag, make sure to run migrations as it may require making changes to the database. +This configuration defines additional options to pass to [express-session](https://www.npmjs.com/package/express-session), which is used to store the Medusa server's session. -::: +#### Example +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + sessionOptions: { + name: process.env.SESSION_NAME || "custom", + } + // ... + }, + // ... +}) +``` -# Admin Components +#### Properties -In this section, you'll find examples of implementing common Medusa Admin components and layouts. +- name: (\`string\`) The name of the session ID cookie to set in the response (and read from in the request). The default value is \`connect.sid\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#name) for more details. +- resave: (\`boolean\`) Whether the session should be saved back to the session store, even if the session was never modified during the request. The default value is \`true\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#resave) for more details. +- rolling: (\`boolean\`) Whether the session identifier cookie should be force-set on every response. The default value is \`false\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#rolling) for more details. +- saveUninitialized: (\`boolean\`) Whether a session that is "uninitialized" is forced to be saved to the store. The default value is \`true\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#saveUninitialized) for more details. +- secret: (\`string\`) The secret to sign the session ID cookie. By default, the value of \`http.cookieSecret\` is used. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#secret) for details. +- ttl: (\`number\`) Used when calculating the \`Expires\` \`Set-Cookie\` attribute of cookies. By default, its value is \`10 \* 60 \* 60 \* 1000\`. + Refer to \[express-session’s documentation]\(https://www.npmjs.com/package/express-session#cookiemaxage) for details. -These components are useful to follow the same design conventions as the Medusa Admin, and are build on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). +### workerMode -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components. +Configure the application's worker mode. -## Layouts +Workers are processes running separately from the main application. They're useful for executing long-running or resource-heavy tasks in the background, such as importing products. -Use these components to set the layout of your UI route. +With a worker, these tasks are offloaded to a separate process. So, they won't affect the performance of the main application. -*** +![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) -## Components +Medusa has three runtime modes: -Use these components in your widgets and UI routes. +- Use `shared` to run the application in a single process. +- Use `worker` to run the a worker process only. +- Use `server` to run the application server only. +In production, it's recommended to deploy two instances: -# Action Menu - Admin Components +1. One having the `workerMode` configuration set to `server`. +2. Another having the `workerMode` configuration set to `worker`. -The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. +#### Example -![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + workerMode: process.env.WORKER_MODE || "shared" + // ... + }, + // ... +}) +``` -To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: +### http -```tsx title="src/admin/components/action-menu.tsx" -import { - DropdownMenu, - IconButton, - clx, -} from "@medusajs/ui" -import { EllipsisHorizontal } from "@medusajs/icons" -import { Link } from "react-router-dom" +This property configures the application's http-specific settings. -export type Action = { - icon: React.ReactNode - label: string - disabled?: boolean -} & ( - | { - to: string - onClick?: never - } - | { - onClick: () => void - to?: never +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + http: { + cookieSecret: "supersecret", + compression: { + // ... + } } -) + // ... + }, + // ... +}) +``` -export type ActionGroup = { - actions: Action[] -} +#### Properties -export type ActionMenuProps = { - groups: ActionGroup[] -} +- authCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. -export const ActionMenu = ({ groups }: ActionMenuProps) => { - return ( - - - - - - - - {groups.map((group, index) => { - if (!group.actions.length) { - return null - } + \`cors\` is a string used to specify the accepted URLs or patterns for API Routes starting with \`/auth\`. It can either be one accepted origin, or a comma-separated list of accepted origins. - const isLast = index === groups.length - 1 + Every origin in that list must either be: - return ( - - {group.actions.map((action, index) => { - if (action.onClick) { - return ( - { - e.stopPropagation() - action.onClick() - }} - className={clx( - "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - } - )} - > - {action.icon} - {action.label} - - ) - } + 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that Medusa tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- storeCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. - return ( -
- - e.stopPropagation()}> - {action.icon} - {action.label} - - -
- ) - })} - {!isLast && } -
- ) - })} -
-
- ) -} -``` + \`store\_cors\` is a string used to specify the accepted URLs or patterns for store API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. -The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. + Every origin in that list must either be: -When the button is clicked, a dropdown menu is shown with the actions passed in the props. + 1\. A URL. For example, \`http://localhost:8000\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- adminCors: (\`string\`) The Medusa application's API Routes are protected by Cross-Origin Resource Sharing (CORS). So, only allowed URLs or URLs matching a specified pattern can send requests to the backend’s API Routes. -The component accepts the following props: + \`admin\_cors\` is a string used to specify the accepted URLs or patterns for admin API Routes. It can either be one accepted origin, or a comma-separated list of accepted origins. -- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. + Every origin in that list must either be: - - actions: (\`object\[]\`) Actions in the group. + 1\. A URL. For example, \`http://localhost:7001\`. The URL must not end with a backslash; + 2\. Or a regular expression pattern that can match more than one origin. For example, \`.example.com\`. The regex pattern that the backend tests for is \`^(\[/~@;%#'])(.\*?)\1(\[gimsuy]\*)$\`. +- jwtSecret: (\`string\`) A random string used to create authentication tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. - - icon: (\`React.ReactNode\`) + In a development environment, if this option is not set the default secret is \`supersecret\`. However, in production, if this configuration is not set, an + error is thrown and the application crashes. +- jwtExpiresIn: (\`string\`) The expiration time for the JWT token. Its format is based off the \[ms package]\(https://github.com/vercel/ms). - - label: (\`string\`) The action's text. + If not provided, the default value is \`24h\`. +- cookieSecret: (\`string\`) A random string used to create cookie tokens in the http layer. Although this configuration option is not required, it’s highly recommended to set it for better security. - - disabled: (\`boolean\`) Whether the action is shown as disabled. + In a development environment, if this option is not set, the default secret is \`supersecret\`. However, in production, if this configuration is not set, an error is thrown and + the application crashes. +- compression: (\[HttpCompressionOptions]\(../medusa\_config.HttpCompressionOptions/page.mdx)) Configure HTTP compression from the application layer. If you have access to the HTTP server, the recommended approach would be to enable it there. + However, some platforms don't offer access to the HTTP layer and in those cases, this is a good alternative. - - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. + If you enable HTTP compression and you want to disable it for specific API Routes, you can pass in the request header \`"x-no-compression": true\`. + Learn more in the \[API Reference]\(https://docs.medusajs.com/api/store#http-compression). - - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. + - enabled: (\`boolean\`) Whether HTTP compression is enabled. By default, it's \`false\`. -*** + - level: (\`number\`) The level of zlib compression to apply to responses. A higher level will result in better compression but will take longer to complete. + A lower level will result in less compression but will be much faster. The default value is \`6\`. -## Example + - memLevel: (\`number\`) How much memory should be allocated to the internal compression state. It's an integer in the range of 1 (minimum level) and 9 (maximum level). + The default value is \`8\`. -Use the `ActionMenu` component in any widget or UI route. + - threshold: (\`string\` \\| \`number\`) The minimum response body size that compression is applied on. Its value can be the number of bytes or any string accepted by the + \[bytes]\(https://www.npmjs.com/package/bytes) module. The default value is \`1024\`. +- authMethodsPerActor: (\`Record\\`) This configuration specifies the supported authentication providers per actor type (such as \`user\`, \`customer\`, or any custom actors). + For example, you only want to allow SSO logins for \`users\`, while you want to allow email/password logins for \`customers\` to the storefront. -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + \`authMethodsPerActor\` is a a map where the actor type (eg. 'user') is the key, and the value is an array of supported auth provider IDs. +- restrictedFields: (\`object\`) Specifies the fields that can't be selected in the response unless specified in the allowed query config. + This is useful to restrict sensitive fields from being exposed in the API. -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Pencil } from "@medusajs/icons" -import { Container } from "../components/container" -import { ActionMenu } from "../components/action-menu" + - store: (\`string\`\[]) -const ProductWidget = () => { - return ( - - , - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ]} /> - - ) -} +*** -export const config = defineWidgetConfig({ - zone: "product.details.before", +## admin + +This property holds configurations for the Medusa Admin dashboard. + +### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + backendUrl: process.env.MEDUSA_BACKEND_URL || + "http://localhost:9000" + }, + // ... }) +``` -export default ProductWidget +### disable + +Whether to disable the admin dashboard. If set to `true`, the admin dashboard is disabled, +in both development and production environments. The default value is `false`. + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false + }, + // ... +}) ``` -This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. +### path -### Use in Header +The path to the admin dashboard. The default value is `/app`. -You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. +The value cannot be one of the reserved paths: -For example: +- `/admin` +- `/store` +- `/auth` +- `/` -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Pencil } from "@medusajs/icons" -import { Container } from "../components/container" -import { Header } from "../components/header" +:::note -const ProductWidget = () => { - return ( - -
, - label: "Edit", - onClick: () => { - alert("You clicked the edit action!") - }, - }, - ], - }, - ], - }, - }, - ]} - /> - - ) -} +When using Docker, make sure that the root path of the Docker image doesn't path the admin's `path`. For example, if the Docker image's root path is `/app`, change +the value of the `path` configuration, as it's `/app` by default. -export const config = defineWidgetConfig({ - zone: "product.details.before", +::: + +#### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + path: process.env.ADMIN_PATH || `/app`, + }, + // ... }) - -export default ProductWidget ``` +### outDir -# Data Table - Admin Components +The directory where the admin build is outputted when you run the `build` command. +The default value is `./build`. -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). +#### Example -The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + outDir: process.env.ADMIN_BUILD_DIR || `./build`, + }, + // ... +}) +``` -You can use this component in your Admin Extensions to display data in a table format, especially if they're retrieved from API routes of the Medusa application. +### backendUrl -Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. +The URL of your Medusa application. Defaults to the browser origin. This is useful to set when running the admin on a separate domain. -## Example: DataTable with Data Fetching +#### Example -In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts/index.html.md) in a data table with pagination, filtering, searching, and sorting. +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + backendUrl: process.env.MEDUSA_BACKEND_URL || + "http://localhost:9000" + }, + // ... +}) +``` -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: +### vite -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" -import { - HttpTypes, -} from "@medusajs/framework/types" +Configure the Vite configuration for the admin dashboard. This function receives the default Vite configuration +and returns the modified configuration. The default value is `undefined`. -const columnHelper = createDataTableColumnHelper() +*** -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] -``` +## plugins -`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: +On your Medusa server, you can use [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) to add re-usable Medusa customizations. Plugins +can include modules, workflows, API Routes, and other customizations. Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). -1. The column's key in the table's data. -2. An object with the following properties: - - `header`: The column's header. - - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. - - `enableSorting`: (optional) A boolean that enables sorting data by this column. - - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. - - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". - - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". +Aside from installing the plugin with NPM, you need to pass the plugin you installed into the `plugins` array defined in `medusa-config.ts`. -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. +The items in the array can either be: -To define the filters, add the following: +- A string, which is the name of the plugin's package as specified in the plugin's `package.json` file. You can pass a plugin as a string if it doesn’t require any options. +- An object having the following properties: + - `resolve`: The name of the plugin's package as specified in the plugin's `package.json` file. + - `options`: An object that includes options to be passed to the modules within the plugin. Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, -} from "@medusajs/ui" +Learn how to create a plugin in [this documentation](https://docs.medusajs.com/learn/fundamentals/plugins/create/index.html.md). -const filterHelper = createDataTableFilterHelper() +### Example -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", +```ts title="medusa-config.ts" +module.exports = { + plugins: [ + `medusa-my-plugin-1`, + { + resolve: `medusa-my-plugin`, + options: { + apiKey: process.env.MY_API_KEY || + `test`, }, - ], - }), -] + }, + // ... + ], + // ... +} ``` -`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: +### resolve -1. The key of a column in the table's data. -2. An object with the following properties: - - `type`: The type of filter. It can be either: - - `select`: A select dropdown allowing users to choose multiple values. - - `radio`: A radio button allowing users to choose one value. - - `date`: A date picker allowing users to choose a date. - - `label`: The filter's label. - - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. +The name of the plugin's package as specified in the plugin's `package.json` file. -You'll now start creating the UI widget's component. Start by adding the necessary state variables: +### options -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" +An object that includes options to be passed to the modules within the plugin. +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). -// ... +*** -const limit = 15 +## modules -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) +This property holds all custom modules installed in your Medusa application. - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) +:::note - // TODO add data fetching logic -} +Medusa's commerce modules are configured by default, so only +add them to this property if you're changing their configurations or adding providers to a module. + +::: + +`modules` is an array of objects, each holding a module's registration configurations. Each object has the following properties: + +1. `resolve`: a string indicating the path to the module relative to `src`, or the module's NPM package name. For example, `./modules/my-module`. +2. `options`: (optional) an object indicating the options to pass to the module. + +### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + modules: [ + { + resolve: "./modules/hello" + } + ] + // ... +}) ``` -In the component, you've added the following state variables: +*** -- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: - - `pageSize`: The number of items to show per page. - - `pageIndex`: The current page index. -- `search`: A string that holds the search query. -- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. -- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. +## featureFlags -You've also added two memoized variables: +Some features in the Medusa application are guarded by a feature flag. This ensures constant shipping of new features while maintaining the engine’s stability. -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. +You can enable a feature in your application by enabling its feature flag. Feature flags are enabled through either environment +variables or through this configuration property exported in `medusa-config.ts`. -Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: +The `featureFlags`'s value is an object. Its properties are the names of the feature flags, and their value is a boolean indicating whether the feature flag is enabled. -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" +You can find available feature flags and their key name [here](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/loaders/feature-flags). + +### Example + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + featureFlags: { + analytics: true, + // ... + } + // ... +}) ``` -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). +:::note -Then, replace the `TODO` in the component with the following: +After enabling a feature flag, make sure to run migrations as it may require making changes to the database. -```tsx title="src/admin/routes/custom/page.tsx" -const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], -}) +::: + + +# Admin Components + +In this section, you'll find examples of implementing common Medusa Admin components and layouts. + +These components are useful to follow the same design conventions as the Medusa Admin, and are build on top of the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md). + +Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.md) for a full list of components. -// TODO configure data table -``` +## Layouts -You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: +Use these components to set the layout of your UI route. -- `limit`: The number of products to fetch per page. -- `offset`: The number of products to skip based on the current page. -- `q`: The search query, if set. -- `status`: The status filters, if set. -- `order`: The sorting order, if set. +*** -So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. +## Components -Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: +Use these components in your widgets and UI routes. -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -``` -Then, replace the `TODO` in the component with the following: +# Single Column Layout - Admin Components -```tsx title="src/admin/routes/custom/page.tsx" -const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, -}) +The Medusa Admin has pages with a single column of content. -// TODO render component -``` +This doesn't include the sidebar, only the main content. -The `useDataTable` hook accepts an object with the following properties: +![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) -- `columns`: The columns to display in the data table. You created this using the `createDataTableColumnHelper` utility. -- `data`: The products fetched from the Medusa application. -- `getRowId`: A function that returns the unique ID of a row. -- `rowCount`: The total number of products that can be retrieved. This is used to determine the number of pages. -- `isLoading`: A boolean that indicates if the data is being fetched. -- `pagination`: An object to configure pagination. It accepts with the following properties: - - `state`: The pagination React state variable. - - `onPaginationChange`: A function that updates the pagination state. -- `search`: An object to configure searching. It accepts the following properties: - - `state`: The search query React state variable. - - `onSearchChange`: A function that updates the search query state. -- `filtering`: An object to configure filtering. It accepts the following properties: - - `state`: The filtering React state variable. - - `onFilteringChange`: A function that updates the filtering state. -- `filters`: The filters to display in the data table. You created this using the `createDataTableFilterHelper` utility. -- `sorting`: An object to configure sorting. It accepts the following properties: - - `state`: The sorting React state variable. - - `onSortingChange`: A function that updates the sorting state. +To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: -Finally, you'll render the data table. But first, add the following imports at the top of the page: +```tsx title="src/admin/layouts/single-column.tsx" +export type SingleColumnLayoutProps = { + children: React.ReactNode +} -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" +export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { + return ( +
+ {children} +
+ ) +} ``` -Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - -Then, replace the `TODO` in the component with the following: +The `SingleColumnLayout` accepts the content in the `children` props. -```tsx title="src/admin/routes/custom/page.tsx" -return ( - - - - - Products -
- - - -
-
- - -
-
-
-) -``` +*** -You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. +## Example -Lastly, export the component and the UI widget's configuration at the end of the file: +Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} import { defineRouteConfig } from "@medusajs/admin-sdk" import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container } from "../../components/container" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Header } from "../../components/header" -// ... +const CustomPage = () => { + return ( + + +
+ + + ) +} export const config = defineRouteConfig({ label: "Custom", @@ -27862,150 +27385,74 @@ export const config = defineRouteConfig({ export default CustomPage ``` -If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. +This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components. -### Full Example Code -```tsx title="src/admin/routes/custom/page.tsx" -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { - Badge, - createDataTableColumnHelper, - createDataTableFilterHelper, - DataTable, - DataTableFilteringState, - DataTablePaginationState, - DataTableSortingState, - Heading, - useDataTable, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { SingleColumnLayout } from "../../layouts/single-column" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" -import { Container } from "../../components/container" -import { HttpTypes, ProductStatus } from "@medusajs/framework/types" +# Two Column Layout - Admin Components -const columnHelper = createDataTableColumnHelper() +The Medusa Admin has pages with two columns of content. -const columns = [ - columnHelper.accessor("title", { - header: "Title", - // Enables sorting for the column. - enableSorting: true, - // If omitted, the header will be used instead if it's a string, - // otherwise the accessor key (id) will be used. - sortLabel: "Title", - // If omitted the default value will be "A-Z" - sortAscLabel: "A-Z", - // If omitted the default value will be "Z-A" - sortDescLabel: "Z-A", - }), - columnHelper.accessor("status", { - header: "Status", - cell: ({ getValue }) => { - const status = getValue() - return ( - - {status === "published" ? "Published" : "Draft"} - - ) - }, - }), -] +This doesn't include the sidebar, only the main content. -const filterHelper = createDataTableFilterHelper() +![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] +To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: -const limit = 15 +```tsx title="src/admin/layouts/two-column.tsx" +export type TwoColumnLayoutProps = { + firstCol: React.ReactNode + secondCol: React.ReactNode +} -const CustomPage = () => { - const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, - }) - const [search, setSearch] = useState("") - const [filtering, setFiltering] = useState({}) - const [sorting, setSorting] = useState(null) +export const TwoColumnLayout = ({ + firstCol, + secondCol, +}: TwoColumnLayoutProps) => { + return ( +
+
+ {firstCol} +
+
+ {secondCol} +
+
+ ) +} +``` - const offset = useMemo(() => { - return pagination.pageIndex * limit - }, [pagination]) - const statusFilters = useMemo(() => { - return (filtering.status || []) as ProductStatus - }, [filtering]) +The `TwoColumnLayout` accepts two props: - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - q: search, - status: statusFilters, - order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, - }), - queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], - }) +- `firstCol` indicating the content of the first column. +- `secondCol` indicating the content of the second column. - const table = useDataTable({ - columns, - data: data?.products || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, - search: { - state: search, - onSearchChange: setSearch, - }, - filtering: { - state: filtering, - onFilteringChange: setFiltering, - }, - filters, - sorting: { - // Pass the pagination state and updater to the table instance - state: sorting, - onSortingChange: setSorting, - }, - }) +*** + +## Example + +Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: +```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { Container } from "../../components/container" +import { Header } from "../../components/header" +import { TwoColumnLayout } from "../../layouts/two-column" + +const CustomPage = () => { return ( - - - - - Products -
- - - -
-
- - -
-
-
+ +
+ + } + secondCol={ + +
+ + } + /> ) } @@ -28017,6 +27464,8 @@ export const config = defineRouteConfig({ export default CustomPage ``` +This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. + # Container - Admin Components @@ -28042,27 +27491,254 @@ export const Container = (props: ContainerProps) => { )} /> ) } -``` +``` + +The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. + +*** + +## Example + +Use that `Container` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
+ + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + + +# Action Menu - Admin Components + +The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. + +![Example of an action menu in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728291319/Medusa%20Resources/action-menu_jnus6k.png) + +To create a component that shows this menu in your customizations, create the file `src/admin/components/action-menu.tsx` with the following content: + +```tsx title="src/admin/components/action-menu.tsx" +import { + DropdownMenu, + IconButton, + clx, +} from "@medusajs/ui" +import { EllipsisHorizontal } from "@medusajs/icons" +import { Link } from "react-router-dom" + +export type Action = { + icon: React.ReactNode + label: string + disabled?: boolean +} & ( + | { + to: string + onClick?: never + } + | { + onClick: () => void + to?: never + } +) + +export type ActionGroup = { + actions: Action[] +} + +export type ActionMenuProps = { + groups: ActionGroup[] +} + +export const ActionMenu = ({ groups }: ActionMenuProps) => { + return ( + + + + + + + + {groups.map((group, index) => { + if (!group.actions.length) { + return null + } + + const isLast = index === groups.length - 1 + + return ( + + {group.actions.map((action, index) => { + if (action.onClick) { + return ( + { + e.stopPropagation() + action.onClick() + }} + className={clx( + "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", + { + "[&_svg]:text-ui-fg-disabled": action.disabled, + } + )} + > + {action.icon} + {action.label} + + ) + } + + return ( +
+ + e.stopPropagation()}> + {action.icon} + {action.label} + + +
+ ) + })} + {!isLast && } +
+ ) + })} +
+
+ ) +} +``` + +The `ActionMenu` component shows a three-dots icon (or `EllipsisHorizontal`) from the [Medusa Icons package](https://docs.medusajs.com/ui/icons/overview/index.html.md) in a button. + +When the button is clicked, a dropdown menu is shown with the actions passed in the props. + +The component accepts the following props: + +- groups: (\`object\[]\`) Groups of actions to be shown in the dropdown. Each group is separated by a divider. + + - actions: (\`object\[]\`) Actions in the group. + + - icon: (\`React.ReactNode\`) + + - label: (\`string\`) The action's text. + + - disabled: (\`boolean\`) Whether the action is shown as disabled. + + - \`to\`: (\`string\`) The link to take the user to when they click the action. This is required if \`onClick\` isn't provided. + + - \`onClick\`: (\`() => void\`) The function to execute when the action is clicked. This is required if \`to\` isn't provided. + +*** + +## Example + +Use the `ActionMenu` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Pencil } from "@medusajs/icons" +import { Container } from "../components/container" +import { ActionMenu } from "../components/action-menu" + +const ProductWidget = () => { + return ( + + , + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ]} /> + + ) +} -The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) -*** +export default ProductWidget +``` -## Example +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. -Use that `Container` component in any widget or UI route. +### Use in Header -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +You can also use the action menu in the [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) component as part of its actions. + +For example: ```tsx title="src/admin/widgets/product-widget.tsx" import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Pencil } from "@medusajs/icons" import { Container } from "../components/container" import { Header } from "../components/header" const ProductWidget = () => { return ( -
+
, + label: "Edit", + onClick: () => { + alert("You clicked the edit action!") + }, + }, + ], + }, + ], + }, + }, + ]} + /> ) } @@ -28074,8 +27750,6 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - # Forms - Admin Components @@ -28650,378 +28324,306 @@ This component uses the [Container](https://docs.medusajs.com/Users/shahednasser It will add at the top of a product's details page a new section, and in its header you'll find an "Edit Item" button. If you click on it, it will open the drawer with your form. -# Header - Admin Components - -Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. - -![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) - -To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: - -```tsx title="src/admin/components/header.tsx" -import { Heading, Button, Text } from "@medusajs/ui" -import React from "react" -import { Link, LinkProps } from "react-router-dom" -import { ActionMenu, ActionMenuProps } from "./action-menu" - -export type HeadingProps = { - title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] -} - -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { - return ( -
-
- {title} - {subtitle && ( - - {subtitle} - - )} -
- {actions.length > 0 && ( -
- {actions.map((action, index) => ( - <> - {action.type === "button" && ( - - )} - {action.type === "action-menu" && ( - - )} - {action.type === "custom" && action.children} - - ))} -
- )} -
- ) -} -``` - -The `Header` component shows a title, and optionally a subtitle and action buttons. +# Data Table - Admin Components -The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). -It accepts the following props: +The [DataTable component in Medusa UI](https://docs.medusajs.com/ui/components/data-table/index.html.md) allows you to display data in a table with sorting, filtering, and pagination. -- title: (\`string\`) The section's title. -- subtitle: (\`string\`) The section's subtitle. -- actions: (\`object\[]\`) An array of actions to show. +You can use this component in your Admin Extensions to display data in a table format, especially if they're retrieved from API routes of the Medusa application. - - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. +Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/components/data-table/index.html.md) for detailed information about the DataTable component and its different usages. - \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. +## Example: DataTable with Data Fetching - \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. +In this example, you'll create a UI widget that shows the list of products retrieved from the [List Products API Route](https://docs.medusajs.com/api/admin#products_getproducts) in a data table with pagination, filtering, searching, and sorting. - \- If its value is \`custom\`, you can pass any React nodes to render. +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: - - props: (object) +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" - - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. +const columnHelper = createDataTableColumnHelper() -*** +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] +``` -## Example +`createDataTableColumnHelper` utility creates a column helper that helps you define the columns for the data table. The column helper has an `accessor` method that accepts two parameters: -Use the `Header` component in any widget or UI route. +1. The column's key in the table's data. +2. An object with the following properties: + - `header`: The column's header. + - `cell`: (optional) By default, a data's value for a column is displayed as a string. Use this property to specify custom rendering of the value. It accepts a function that returns a string or a React node. The function receives an object that has a `getValue` property function to retrieve the raw value of the cell. + - `enableSorting`: (optional) A boolean that enables sorting data by this column. + - `sortLabel`: (optional) The label for the sorting button. If omitted, the `header` will be used instead if it's a string, otherwise the accessor key (id) will be used. + - `sortAscLabel`: (optional) The label for the ascending sorting button. If omitted, the default value will be "A-Z". + - `sortDescLabel`: (optional) The label for the descending sorting button. If omitted, the default value will be "Z-A". -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" +To define the filters, add the following: -const ProductWidget = () => { - return ( - -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> - - ) -} +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +const filterHelper = createDataTableFilterHelper() -export default ProductWidget +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] ``` -This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. +`createDataTableFilterHelper` utility creates a filter helper that helps you define the filters for the data table. The filter helper has an `accessor` method that accepts two parameters: +1. The key of a column in the table's data. +2. An object with the following properties: + - `type`: The type of filter. It can be either: + - `select`: A select dropdown allowing users to choose multiple values. + - `radio`: A radio button allowing users to choose one value. + - `date`: A date picker allowing users to choose a date. + - `label`: The filter's label. + - `options`: An array of objects with `label` and `value` properties. The `label` is the option's label, and the `value` is the value to filter by. -# Table - Admin Components +You'll now start creating the UI widget's component. Start by adding the necessary state variables: -If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead. +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" -The listing pages in the Admin show a table with pagination. +// ... -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) +const limit = 15 -To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content: +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) -```tsx title="src/admin/components/table.tsx" -import { useMemo } from "react" -import { Table as UiTable } from "@medusajs/ui" + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) -export type TableProps = { - columns: { - key: string - label?: string - render?: (value: unknown) => React.ReactNode - }[] - data: Record[] - pageSize: number - count: number - currentPage: number - setCurrentPage: (value: number) => void + // TODO add data fetching logic } +``` -export const Table = ({ - columns, - data, - pageSize, - count, - currentPage, - setCurrentPage, -}: TableProps) => { - const pageCount = useMemo(() => { - return Math.ceil(count / pageSize) - }, [data, pageSize]) - - const canNextPage = useMemo(() => { - return currentPage < pageCount - 1 - }, [currentPage, pageCount]) - const canPreviousPage = useMemo(() => { - return currentPage - 1 >= 0 - }, [currentPage]) - - const nextPage = () => { - if (canNextPage) { - setCurrentPage(currentPage + 1) - } - } - - const previousPage = () => { - if (canPreviousPage) { - setCurrentPage(currentPage - 1) - } - } +In the component, you've added the following state variables: - return ( -
- - - - {columns.map((column, index) => ( - - {column.label || column.key} - - ))} - - - - {data.map((item, index) => { - const rowIndex = "id" in item ? item.id as string : index - return ( - - {columns.map((column, index) => ( - - <> - {column.render && column.render(item[column.key])} - {!column.render && ( - <>{item[column.key] as string} - )} - - - ))} - - ) - })} - - - -
- ) -} -``` +- `pagination`: An object of type `DataTablePaginationState` that holds the pagination state. It has two properties: + - `pageSize`: The number of items to show per page. + - `pageIndex`: The current page index. +- `search`: A string that holds the search query. +- `filtering`: An object of type `DataTableFilteringState` that holds the filtering state. +- `sorting`: An object of type `DataTableSortingState` that holds the sorting state. -The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data. +You've also added two memoized variables: -It accepts the following props: +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. -- columns: (\`object\[]\`) The table's columns. +Next, you'll fetch the products from the Medusa application. Assuming you have the JS SDK configured as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), add the following imports at the top of the file: - - key: (\`string\`) The column's key in the passed \`data\` +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" +``` - - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node. -- data: (\`Record\\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array. -- pageSize: (\`number\`) The number of items to show per page. -- count: (\`number\`) The total number of items. -- currentPage: (\`number\`) A zero-based index indicating the current page's number. -- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page. +Then, replace the `TODO` in the component with the following: -*** +```tsx title="src/admin/routes/custom/page.tsx" +const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], +}) -## Example +// TODO configure data table +``` -Use the `Table` component in any widget or UI route. +You use the `useQuery` hook to fetch the products from the Medusa application. In the `queryFn`, you call the `sdk.admin.product.list` method to fetch the products. You pass the following query parameters to the method: -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: +- `limit`: The number of products to fetch per page. +- `offset`: The number of products to skip based on the current page. +- `q`: The search query, if set. +- `status`: The status filters, if set. +- `order`: The sorting order, if set. -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { StatusBadge } from "@medusajs/ui" -import { Table } from "../components/table" -import { useState } from "react" -import { Container } from "../components/container" +So, whenever the user changes the current page, search query, status filters, or sorting, the products are fetched based on the new parameters. -const ProductWidget = () => { - const [currentPage, setCurrentPage] = useState(0) +Next, you'll configure the data table. Medusa UI provides a `useDataTable` hook that helps you configure the data table. Add the following imports at the top of the file: - return ( - - { - const isEnabled = value as boolean +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + useDataTable, +} from "@medusajs/ui" +``` - return ( - - {isEnabled ? "Enabled" : "Disabled"} - - ) - }, - }, - ]} - data={[ - { - name: "John", - is_enabled: true, - }, - { - name: "Jane", - is_enabled: false, - }, - ]} - pageSize={2} - count={2} - currentPage={currentPage} - setCurrentPage={setCurrentPage} - /> - - ) -} +Then, replace the `TODO` in the component with the following: -export const config = defineWidgetConfig({ - zone: "product.details.before", +```tsx title="src/admin/routes/custom/page.tsx" +const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, }) -export default ProductWidget +// TODO render component ``` -This widget also uses the [Container](../container.mdx) custom component. - -*** - -## Example With Data Fetching +The `useDataTable` hook accepts an object with the following properties: -This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. +- `columns`: The columns to display in the data table. You created this using the `createDataTableColumnHelper` utility. +- `data`: The products fetched from the Medusa application. +- `getRowId`: A function that returns the unique ID of a row. +- `rowCount`: The total number of products that can be retrieved. This is used to determine the number of pages. +- `isLoading`: A boolean that indicates if the data is being fetched. +- `pagination`: An object to configure pagination. It accepts with the following properties: + - `state`: The pagination React state variable. + - `onPaginationChange`: A function that updates the pagination state. +- `search`: An object to configure searching. It accepts the following properties: + - `state`: The search query React state variable. + - `onSearchChange`: A function that updates the search query state. +- `filtering`: An object to configure filtering. It accepts the following properties: + - `state`: The filtering React state variable. + - `onFilteringChange`: A function that updates the filtering state. +- `filters`: The filters to display in the data table. You created this using the `createDataTableFilterHelper` utility. +- `sorting`: An object to configure sorting. It accepts the following properties: + - `state`: The sorting React state variable. + - `onSortingChange`: A function that updates the sorting state. -Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content: +Finally, you'll render the data table. But first, add the following imports at the top of the page: -```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { useQuery } from "@tanstack/react-query" +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + DataTable, +} from "@medusajs/ui" import { SingleColumnLayout } from "../../layouts/single-column" -import { Table } from "../../components/table" -import { sdk } from "../../lib/config" -import { useMemo, useState } from "react" import { Container } from "../../components/container" -import { Header } from "../../components/header" +``` -const CustomPage = () => { - const [currentPage, setCurrentPage] = useState(0) - const limit = 15 - const offset = useMemo(() => { - return currentPage * limit - }, [currentPage]) +Aside from the `DataTable` component, you also import the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md) and [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) components implemented in other Admin Component guides. These components ensure a style consistent to other pages in the admin dashboard. - const { data } = useQuery({ - queryFn: () => sdk.admin.product.list({ - limit, - offset, - }), - queryKey: [["products", limit, offset]], - }) +Then, replace the `TODO` in the component with the following: - // TODO display table -} +```tsx title="src/admin/routes/custom/page.tsx" +return ( + + + + + Products +
+ + + +
+
+ + +
+
+
+) +``` + +You render the `DataTable` component and pass the `table` instance as a prop. In the `DataTable` component, you render a toolbar showing a heading, filter menu, sorting menu, and a search input. You also show pagination after the table. + +Lastly, export the component and the UI widget's configuration at the end of the file: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" + +// ... export const config = defineRouteConfig({ label: "Custom", @@ -29031,127 +28633,267 @@ export const config = defineRouteConfig({ export default CustomPage ``` -In the `CustomPage` component, you define: +If you start your Medusa application and go to `localhost:9000/app/custom`, you'll see the data table showing the list of products with pagination, filtering, searching, and sorting functionalities. -- A state variable `currentPage` that stores the current page of the table. -- A `limit` variable, indicating how many items to retrieve per page -- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`. +### Full Example Code -Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page. +```tsx title="src/admin/routes/custom/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { ChatBubbleLeftRight } from "@medusajs/icons" +import { + Badge, + createDataTableColumnHelper, + createDataTableFilterHelper, + DataTable, + DataTableFilteringState, + DataTablePaginationState, + DataTableSortingState, + Heading, + useDataTable, +} from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" +import { Container } from "../../components/container" +import { HttpTypes, ProductStatus } from "@medusajs/framework/types" -You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md). +const columnHelper = createDataTableColumnHelper() -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. +const columns = [ + columnHelper.accessor("title", { + header: "Title", + // Enables sorting for the column. + enableSorting: true, + // If omitted, the header will be used instead if it's a string, + // otherwise the accessor key (id) will be used. + sortLabel: "Title", + // If omitted the default value will be "A-Z" + sortAscLabel: "A-Z", + // If omitted the default value will be "Z-A" + sortDescLabel: "Z-A", + }), + columnHelper.accessor("status", { + header: "Status", + cell: ({ getValue }) => { + const status = getValue() + return ( + + {status === "published" ? "Published" : "Draft"} + + ) + }, + }), +] -`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields. +const filterHelper = createDataTableFilterHelper() -Then, to display the table, replace the `TODO` with the following: +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] -```tsx -return ( - - -
- {data && ( -
- )} - - -) -``` +const limit = 15 -Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md), and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. +const CustomPage = () => { + const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, + }) + const [search, setSearch] = useState("") + const [filtering, setFiltering] = useState({}) + const [sorting, setSorting] = useState(null) + + const offset = useMemo(() => { + return pagination.pageIndex * limit + }, [pagination]) + const statusFilters = useMemo(() => { + return (filtering.status || []) as ProductStatus + }, [filtering]) + + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + q: search, + status: statusFilters, + order: sorting ? `${sorting.desc ? "-" : ""}${sorting.id}` : undefined, + }), + queryKey: [["products", limit, offset, search, statusFilters, sorting?.id, sorting?.desc]], + }) + + const table = useDataTable({ + columns, + data: data?.products || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, + search: { + state: search, + onSearchChange: setSearch, + }, + filtering: { + state: filtering, + onFilteringChange: setFiltering, + }, + filters, + sorting: { + // Pass the pagination state and updater to the table instance + state: sorting, + onSortingChange: setSorting, + }, + }) -If `data` isn't `undefined`, you display the `Table` component passing it the following props: + return ( + + + + + Products +
+ + + +
+
+ + +
+
+
+ ) +} -- `columns`: The columns to show. You only show the product's ID and title. -- `data`: The rows of the table. You pass it the `products` property of `data`. -- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`. -- `currentPage` and `setCurrentPage`: The current page and the function to change it. +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) -To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. +export default CustomPage +``` -# Section Row - Admin Components +# Header - Admin Components -The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. +Each section in the Medusa Admin has a header with a title, and optionally a subtitle with buttons to perform an action. -![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) +![Example of a header in a section](https://res.cloudinary.com/dza7lstvk/image/upload/v1728288562/Medusa%20Resources/header_dtz4gl.png) -To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: +To create a component that uses the same header styling and structure, create the file `src/admin/components/header.tsx` with the following content: -```tsx title="src/admin/components/section-row.tsx" -import { Text, clx } from "@medusajs/ui" +```tsx title="src/admin/components/header.tsx" +import { Heading, Button, Text } from "@medusajs/ui" +import React from "react" +import { Link, LinkProps } from "react-router-dom" +import { ActionMenu, ActionMenuProps } from "./action-menu" -export type SectionRowProps = { +export type HeadingProps = { title: string - value?: React.ReactNode | string | null - actions?: React.ReactNode + subtitle?: string + actions?: ( + { + type: "button", + props: React.ComponentProps + link?: LinkProps + } | + { + type: "action-menu" + props: ActionMenuProps + } | + { + type: "custom" + children: React.ReactNode + } + )[] } -export const SectionRow = ({ title, value, actions }: SectionRowProps) => { - const isValueString = typeof value === "string" || !value - +export const Header = ({ + title, + subtitle, + actions = [], +}: HeadingProps) => { return ( -
- - {title} - - - {isValueString ? ( - - {value ?? "-"} - - ) : ( -
{value}
+
+
+ {title} + {subtitle && ( + + {subtitle} + + )} +
+ {actions.length > 0 && ( +
+ {actions.map((action, index) => ( + <> + {action.type === "button" && ( + + )} + {action.type === "action-menu" && ( + + )} + {action.type === "custom" && action.children} + + ))} +
)} - - {actions &&
{actions}
}
) } ``` -The `SectionRow` component shows a title and a value in the same row. +The `Header` component shows a title, and optionally a subtitle and action buttons. + +The component also uses the [Action Menu](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/action-menu/index.html.md) custom component. It accepts the following props: -- title: (\`string\`) The title to show on the left side. -- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. -- actions: (\`React.ReactNode\`) The actions to show at the end of the row. +- title: (\`string\`) The section's title. +- subtitle: (\`string\`) The section's subtitle. +- actions: (\`object\[]\`) An array of actions to show. + + - type: (\`button\` \\| \`action-menu\` \\| \`custom\`) The type of action to add. + + \- If its value is \`button\`, it'll show a button that can have a link or an on-click action. + + \- If its value is \`action-menu\`, it'll show a three dot icon with a dropdown of actions. + + \- If its value is \`custom\`, you can pass any React nodes to render. + + - props: (object) + + - children: (React.ReactNode) This property is only accepted if \`type\` is \`custom\`. Its content is rendered as part of the actions. *** ## Example -Use the `SectionRow` component in any widget or UI route. +Use the `Header` component in any widget or UI route. For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: @@ -29159,13 +28901,26 @@ For example, create the widget `src/admin/widgets/product-widget.tsx` with the f import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container } from "../components/container" import { Header } from "../components/header" -import { SectionRow } from "../components/section-row" const ProductWidget = () => { return ( -
- +
{ + alert("You clicked the button.") + }, + }, + }, + ]} + /> ) } @@ -29177,7 +28932,7 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. +This widget also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) custom component. # JSON View - Admin Components @@ -29368,34 +29123,126 @@ const Copied = ({ style, value }: CopiedProps) => { ) } - return ( - - - + return ( + + + + ) +} +``` + +The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. + +The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. + +*** + +## Example + +Use the `JsonViewSection` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { JsonViewSection } from "../components/json-view-section" + +const ProductWidget = () => { + return +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. + + +# Section Row - Admin Components + +The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. + +![Example of a section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) + +To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: + +```tsx title="src/admin/components/section-row.tsx" +import { Text, clx } from "@medusajs/ui" + +export type SectionRowProps = { + title: string + value?: React.ReactNode | string | null + actions?: React.ReactNode +} + +export const SectionRow = ({ title, value, actions }: SectionRowProps) => { + const isValueString = typeof value === "string" || !value + + return ( +
+ + {title} + + + {isValueString ? ( + + {value ?? "-"} + + ) : ( +
{value}
+ )} + + {actions &&
{actions}
} +
) } ``` -The `JsonViewSection` component shows a section with the "JSON" title and a button to show the data as JSON in a drawer or side window. +The `SectionRow` component shows a title and a value in the same row. -The `JsonViewSection` accepts a `data` prop, which is the data to show as a JSON object in the drawer. +It accepts the following props: + +- title: (\`string\`) The title to show on the left side. +- value: (\`React.ReactNode\` \\| \`string\` \\| \`null\`) The value to show on the right side. +- actions: (\`React.ReactNode\`) The actions to show at the end of the row. *** ## Example -Use the `JsonViewSection` component in any widget or UI route. +Use the `SectionRow` component in any widget or UI route. For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: ```tsx title="src/admin/widgets/product-widget.tsx" import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { JsonViewSection } from "../components/json-view-section" +import { Container } from "../components/container" +import { Header } from "../components/header" +import { SectionRow } from "../components/section-row" const ProductWidget = () => { - return + return ( + +
+ + + ) } export const config = defineWidgetConfig({ @@ -29405,135 +29252,234 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -This shows the JSON section at the top of the product page, passing it the object `{ name: "John" }`. +This widget also uses the [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. -# Single Column Layout - Admin Components +# Table - Admin Components -The Medusa Admin has pages with a single column of content. +If you're using [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0), it's recommended to use the [Data Table](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/data-table/index.html.md) component instead. -This doesn't include the sidebar, only the main content. +The listing pages in the Admin show a table with pagination. -![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) -To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: +To create a component that shows a table with pagination, create the file `src/admin/components/table.tsx` with the following content: -```tsx title="src/admin/layouts/single-column.tsx" -export type SingleColumnLayoutProps = { - children: React.ReactNode -} +```tsx title="src/admin/components/table.tsx" +import { useMemo } from "react" +import { Table as UiTable } from "@medusajs/ui" -export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { - return ( -
- {children} -
- ) +export type TableProps = { + columns: { + key: string + label?: string + render?: (value: unknown) => React.ReactNode + }[] + data: Record[] + pageSize: number + count: number + currentPage: number + setCurrentPage: (value: number) => void } -``` -The `SingleColumnLayout` accepts the content in the `children` props. - -*** +export const Table = ({ + columns, + data, + pageSize, + count, + currentPage, + setCurrentPage, +}: TableProps) => { + const pageCount = useMemo(() => { + return Math.ceil(count / pageSize) + }, [data, pageSize]) -## Example + const canNextPage = useMemo(() => { + return currentPage < pageCount - 1 + }, [currentPage, pageCount]) + const canPreviousPage = useMemo(() => { + return currentPage - 1 >= 0 + }, [currentPage]) -Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: + const nextPage = () => { + if (canNextPage) { + setCurrentPage(currentPage + 1) + } + } -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { ChatBubbleLeftRight } from "@medusajs/icons" -import { Container } from "../../components/container" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Header } from "../../components/header" + const previousPage = () => { + if (canPreviousPage) { + setCurrentPage(currentPage - 1) + } + } -const CustomPage = () => { return ( - - -
- - +
+ + + + {columns.map((column, index) => ( + + {column.label || column.key} + + ))} + + + + {data.map((item, index) => { + const rowIndex = "id" in item ? item.id as string : index + return ( + + {columns.map((column, index) => ( + + <> + {column.render && column.render(item[column.key])} + {!column.render && ( + <>{item[column.key] as string} + )} + + + ))} + + ) + })} + + + +
) } +``` -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) +The `Table` component uses the component from the [UI package](https://docs.medusajs.com/ui/components/table/index.html.md), with additional styling and rendering of data. -export default CustomPage -``` +It accepts the following props: -This UI route also uses a [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and a [Header]() custom components. +- columns: (\`object\[]\`) The table's columns. + - key: (\`string\`) The column's key in the passed \`data\` -# Two Column Layout - Admin Components + - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. -The Medusa Admin has pages with two columns of content. + - render: (\`(value: unknown) => React.ReactNode\`) By default, the data is shown as-is in the table. You can use this function to change how the value is rendered. The function receives the value is a parameter and returns a React node. +- data: (\`Record\\[]\`) The data to show in the table for the current page. The keys of each object should be in the \`columns\` array. +- pageSize: (\`number\`) The number of items to show per page. +- count: (\`number\`) The total number of items. +- currentPage: (\`number\`) A zero-based index indicating the current page's number. +- setCurrentPage: (\`(value: number) => void\`) A function used to change the current page. -This doesn't include the sidebar, only the main content. +*** -![An example of an admin page with two columns](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286690/Medusa%20Resources/two-column_sdnkg0.png) +## Example -To create a layout that you can use in UI routes to support two columns of content, create the component `src/admin/layouts/two-column.tsx` with the following content: +Use the `Table` component in any widget or UI route. -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { StatusBadge } from "@medusajs/ui" +import { Table } from "../components/table" +import { useState } from "react" +import { Container } from "../components/container" + +const ProductWidget = () => { + const [currentPage, setCurrentPage] = useState(0) -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { return ( -
-
- {firstCol} -
-
- {secondCol} -
-
+ +
{ + const isEnabled = value as boolean + + return ( + + {isEnabled ? "Enabled" : "Disabled"} + + ) + }, + }, + ]} + data={[ + { + name: "John", + is_enabled: true, + }, + { + name: "Jane", + is_enabled: false, + }, + ]} + pageSize={2} + count={2} + currentPage={currentPage} + setCurrentPage={setCurrentPage} + /> + ) } + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget ``` -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. +This widget also uses the [Container](../container.mdx) custom component. *** -## Example +## Example With Data Fetching -Use the `TwoColumnLayout` component in your UI routes that have a single column. For example: +This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +Assuming you've set up the JS SDK as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), create the UI route `src/admin/routes/custom/page.tsx` with the following content: + +```tsx title="src/admin/routes/custom/page.tsx" collapsibleLines="1-10" expandButtonLabel="Show Imports" highlights={tableExampleHighlights} import { defineRouteConfig } from "@medusajs/admin-sdk" import { ChatBubbleLeftRight } from "@medusajs/icons" +import { useQuery } from "@tanstack/react-query" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Table } from "../../components/table" +import { sdk } from "../../lib/config" +import { useMemo, useState } from "react" import { Container } from "../../components/container" import { Header } from "../../components/header" -import { TwoColumnLayout } from "../../layouts/two-column" const CustomPage = () => { - return ( - -
- - } - secondCol={ - -
- - } - /> - ) + const [currentPage, setCurrentPage] = useState(0) + const limit = 15 + const offset = useMemo(() => { + return currentPage * limit + }, [currentPage]) + + const { data } = useQuery({ + queryFn: () => sdk.admin.product.list({ + limit, + offset, + }), + queryKey: [["products", limit, offset]], + }) + + // TODO display table } export const config = defineRouteConfig({ @@ -29544,7 +29490,61 @@ export const config = defineRouteConfig({ export default CustomPage ``` -This UI route also uses [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md) and [Header]() custom components. +In the `CustomPage` component, you define: + +- A state variable `currentPage` that stores the current page of the table. +- A `limit` variable, indicating how many items to retrieve per page +- An `offset` memoized variable indicating how many items to skip before the retrieved items. It's calculated as a multiplication of `currentPage` and `limit`. + +Then, you use `useQuery` from [Tanstack Query](https://tanstack.com/query/latest) to retrieve products using the JS SDK. You pass `limit` and `offset` as query parameters, and you set the `queryKey`, which is used for caching and revalidation, to be based on the key `products`, along with the current limit and offset. So, whenever the `offset` variable changes, the request is sent again to retrieve the products of the current page. + +You can change the query to send a request to a custom API route as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk#send-requests-to-custom-routes/index.html.md). + +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. + +`useQuery` returns an object containing `data`, which holds the response fields including the products and pagination fields. + +Then, to display the table, replace the `TODO` with the following: + +```tsx +return ( + + +
+ {data && ( +
+ )} + + +) +``` + +Aside from the `Table` component, this UI route also uses the [SingleColumnLayout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/layouts/single-column/index.html.md), [Container](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/container/index.html.md), and [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + +If `data` isn't `undefined`, you display the `Table` component passing it the following props: + +- `columns`: The columns to show. You only show the product's ID and title. +- `data`: The rows of the table. You pass it the `products` property of `data`. +- `pageSize`: The maximum number of items per page. You pass it the `count` property of `data`. +- `currentPage` and `setCurrentPage`: The current page and the function to change it. + +To test it out, log into the Medusa Admin and open `http://localhost:9000/app/custom`. You'll find a table of products with pagination. # Service Factory Reference @@ -29612,46 +29612,6 @@ const posts = await postModuleService.createPosts([ If an array is passed of the method, an array of the created records is also returned. -# delete Method - Service Factory Reference - -This method deletes one or more records. - -## Delete One Record - -```ts -await postModuleService.deletePosts("123") -``` - -To delete one record, pass its ID as a parameter of the method. - -*** - -## Delete Multiple Records - -```ts -await postModuleService.deletePosts([ - "123", - "321", -]) -``` - -To delete multiple records, pass an array of IDs as a parameter of the method. - -*** - -## Delete Records Matching Filters - -```ts -await postModuleService.deletePosts({ - name: "My Post", -}) -``` - -To delete records matching a set of filters, pass an object of filters as a parameter. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - - # list Method - Service Factory Reference This method retrieves a list of records. @@ -29770,61 +29730,44 @@ To sort records by one or more properties, pass to the second object parameter t The method returns an array of the first `15` records matching the filters. -# retrieve Method - Service Factory Reference +# delete Method - Service Factory Reference -This method retrieves one record of the data model by its ID. +This method deletes one or more records. -## Retrieve a Record +## Delete One Record ```ts -const post = await postModuleService.retrievePost("123") +await postModuleService.deletePosts("123") ``` -### Parameters - -Pass the ID of the record to retrieve. - -### Returns - -The method returns the record as an object. +To delete one record, pass its ID as a parameter of the method. *** -## Retrieve a Record's Relations - -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). +## Delete Multiple Records ```ts -const post = await postModuleService.retrievePost("123", { - relations: ["author"], -}) +await postModuleService.deletePosts([ + "123", + "321", +]) ``` -### Parameters - -To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. - -### Returns - -The method returns the record as an object. +To delete multiple records, pass an array of IDs as a parameter of the method. *** -## Select Properties to Retrieve +## Delete Records Matching Filters ```ts -const post = await postModuleService.retrievePost("123", { - select: ["id", "name"], +await postModuleService.deletePosts({ + name: "My Post", }) ``` -### Parameters - -By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. - -### Returns +To delete records matching a set of filters, pass an object of filters as a parameter. -The method returns the record as an object. +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). # restore Method - Service Factory Reference @@ -30037,17 +29980,74 @@ const [posts, count] = await postModuleService.listAndCountPosts({}, { ### Parameters -To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: - -- `ASC` to sort by this property in the ascending order. -- `DESC` to sort by this property in the descending order. +To sort records by one or more properties, pass to the second object parameter the `order` property. Its value is an object whose keys are the property names, and values can either be: + +- `ASC` to sort by this property in the ascending order. +- `DESC` to sort by this property in the descending order. + +### Returns + +The method returns an array with two items: + +1. The first is an array of the first `15` records retrieved. +2. The second is the total count of records. + + +# retrieve Method - Service Factory Reference + +This method retrieves one record of the data model by its ID. + +## Retrieve a Record + +```ts +const post = await postModuleService.retrievePost("123") +``` + +### Parameters + +Pass the ID of the record to retrieve. + +### Returns + +The method returns the record as an object. + +*** + +## Retrieve a Record's Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +```ts +const post = await postModuleService.retrievePost("123", { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. + +### Returns + +The method returns the record as an object. + +*** + +## Select Properties to Retrieve + +```ts +const post = await postModuleService.retrievePost("123", { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. ### Returns -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved. -2. The second is the total count of records. +The method returns the record as an object. # softDelete Method - Service Factory Reference @@ -30137,129 +30137,6 @@ deletedPosts = { ``` -# update Method - Service Factory Reference - -This method updates one or more records of the data model. - -## Update One Record - -```ts -const post = await postModuleService.updatePosts({ - id: "123", - name: "My Post", -}) -``` - -### Parameters - -To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. - -You can pass in the same object any other properties to update. - -### Returns - -The method returns the updated record as an object. - -*** - -## Update Multiple Records - -```ts -const posts = await postModuleService.updatePosts([ - { - id: "123", - name: "My Post", - }, - { - id: "321", - published_at: new Date(), - }, -]) -``` - -### Parameters - -To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. - -You can pass in each object any other properties to update. - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Update Records Matching a Filter - -```ts -const posts = await postModuleService.updatePosts({ - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, -}) -``` - -### Parameters - -To update records that match specified filters, pass as a parameter an object having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of every post record whose name is `My Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - -*** - -## Multiple Record Updates with Filters - -```ts -const posts = await postModuleService.updatePosts([ - { - selector: { - name: "My Post", - }, - data: { - published_at: new Date(), - }, - }, - { - selector: { - name: "Another Post", - }, - data: { - metadata: { - external_id: "123", - }, - }, - }, -]) -``` - -### Parameters - -To update records matching different sets of filters, pass an array of objects, each having two properties: - -- `selector`: An object of filters that a record must match to be updated. -- `data`: An object of the properties to update in every record that match the filters in `selector`. - -In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. - -Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). - -### Returns - -The method returns an array of objects of updated records. - - # Filter Records - Service Factory Reference Many of the service factory's generated methods allow passing filters to perform an operation, such as to update or delete records matching the filters. @@ -30403,6 +30280,129 @@ To use an `or` condition, pass to the filter object the `$or` property, whose va In the example above, posts whose name is `My Post` or their `published_at` date is less than the current date and time are retrieved. +# update Method - Service Factory Reference + +This method updates one or more records of the data model. + +## Update One Record + +```ts +const post = await postModuleService.updatePosts({ + id: "123", + name: "My Post", +}) +``` + +### Parameters + +To update one record, pass an object that at least has an `id` property, identifying the ID of the record to update. + +You can pass in the same object any other properties to update. + +### Returns + +The method returns the updated record as an object. + +*** + +## Update Multiple Records + +```ts +const posts = await postModuleService.updatePosts([ + { + id: "123", + name: "My Post", + }, + { + id: "321", + published_at: new Date(), + }, +]) +``` + +### Parameters + +To update multiple records, pass an array of objects. Each object has at least an `id` property, identifying the ID of the record to update. + +You can pass in each object any other properties to update. + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Update Records Matching a Filter + +```ts +const posts = await postModuleService.updatePosts({ + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, +}) +``` + +### Parameters + +To update records that match specified filters, pass as a parameter an object having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of every post record whose name is `My Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + +*** + +## Multiple Record Updates with Filters + +```ts +const posts = await postModuleService.updatePosts([ + { + selector: { + name: "My Post", + }, + data: { + published_at: new Date(), + }, + }, + { + selector: { + name: "Another Post", + }, + data: { + metadata: { + external_id: "123", + }, + }, + }, +]) +``` + +### Parameters + +To update records matching different sets of filters, pass an array of objects, each having two properties: + +- `selector`: An object of filters that a record must match to be updated. +- `data`: An object of the properties to update in every record that match the filters in `selector`. + +In the example above, you update the `published_at` property of post records whose name is `My Post`, and update the `metadata` property of post records whose name is `Another Post`. + +Learn more about accepted filters in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/tips/filtering/index.html.md). + +### Returns + +The method returns an array of objects of updated records. + +

Just Getting Started?

diff --git a/www/packages/remark-rehype-plugins/src/change-links-md.ts b/www/packages/remark-rehype-plugins/src/change-links-md.ts index f5a74dd884faf..a01879b0c3f2a 100644 --- a/www/packages/remark-rehype-plugins/src/change-links-md.ts +++ b/www/packages/remark-rehype-plugins/src/change-links-md.ts @@ -9,7 +9,9 @@ export const changeLinksToHtmlMdPlugin = (): Transformer => { if ( node.type === "link" && node.url?.startsWith("https://docs.medusajs.com") && - !node.url.endsWith("index.html.md") + !node.url.endsWith("index.html.md") && + !node.url.includes("/api/store") && + !node.url.includes("/api/admin") ) { node.url += `/index.html.md` } diff --git a/www/packages/tags/src/tags/index.ts b/www/packages/tags/src/tags/index.ts index 86c3f8e762bac..28f42e4a0be44 100644 --- a/www/packages/tags/src/tags/index.ts +++ b/www/packages/tags/src/tags/index.ts @@ -1,37 +1,37 @@ export * from "./inventory.js" -export * from "./pricing.js" -export * from "./server.js" +export * from "./concept.js" export * from "./query.js" -export * from "./tax.js" +export * from "./server.js" export * from "./sales-channel.js" -export * from "./stock-location.js" +export * from "./tax.js" +export * from "./cart.js" export * from "./storefront.js" -export * from "./payment.js" export * from "./order.js" -export * from "./fulfillment.js" -export * from "./stripe.js" +export * from "./payment.js" export * from "./product.js" -export * from "./concept.js" -export * from "./product-collection.js" +export * from "./customer.js" export * from "./product-category.js" +export * from "./stripe.js" +export * from "./fulfillment.js" export * from "./publishable-api-key.js" -export * from "./cart.js" -export * from "./region.js" export * from "./api-key.js" -export * from "./customer.js" -export * from "./remote-query.js" -export * from "./link.js" +export * from "./region.js" export * from "./step.js" -export * from "./store.js" -export * from "./promotion.js" -export * from "./workflow.js" +export * from "./product-collection.js" +export * from "./link.js" export * from "./auth.js" -export * from "./event-bus.js" -export * from "./js-sdk.js" -export * from "./notification.js" +export * from "./stock-location.js" +export * from "./workflow.js" +export * from "./pricing.js" export * from "./user.js" +export * from "./remote-query.js" +export * from "./event-bus.js" +export * from "./logger.js" export * from "./locking.js" export * from "./currency.js" -export * from "./file.js" +export * from "./notification.js" +export * from "./js-sdk.js" export * from "./admin.js" -export * from "./logger.js" +export * from "./store.js" +export * from "./promotion.js" +export * from "./file.js"