diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 1c6bd7abf6..48a96512e8 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -141,76 +141,6 @@ The next chapter covers how you generally deploy the production build. You can also refer to the [deployment how-to guides](https://docs.medusajs.com/resources/deployment/index.html.md) for platform-specific how-to guides. -# Medusa Deployment Overview - -In this chapter, you’ll learn the general approach to deploying the Medusa application. - -## Medusa Project Components - -A standard Medusa project is made up of: - -- Medusa application: The Medusa server and the Medusa Admin. -- One or more storefronts - -![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) - -You deploy the Medusa application, with the server and admin, separately from the storefront. - -*** - -## Deploying the Medusa Application - -You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. - -The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… - -![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) - -Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). - -When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. - -### Deploy Server and Worker Instances - -By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. - -Instead, you should deploy two instances: - -- A server instance, which handles incoming requests to the application’s API routes. -- A worker instance, which processes background tasks, including scheduled jobs and subscribers. - -You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. - -Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -### How to Deploy Medusa? - -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. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) - -To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. - -*** - -## Deploying the Storefront - -The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. - -If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. - -Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. - -Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. - - # Install Medusa In this chapter, you'll learn how to install and run a Medusa application. @@ -345,6 +275,98 @@ Refer to [this documentation](https://docs.medusajs.com/learn/update/index.html. 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. +# Medusa Deployment Overview + +In this chapter, you’ll learn the general approach to deploying the Medusa application. + +## Medusa Project Components + +A standard Medusa project is made up of: + +- Medusa application: The Medusa server and the Medusa Admin. +- One or more storefronts + +![Diagram showcasing the connection between the three deployed components](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600807/Medusa%20Book/deployment-options_ceuuvo.jpg) + +You deploy the Medusa application, with the server and admin, separately from the storefront. + +*** + +## Deploying the Medusa Application + +You must deploy the Medusa application before the storefront, as it connects to the server and won’t work without a deployed Medusa server URL. + +The Medusa application must be deployed to a hosting provider supporting Node.js server deployments, such as Railway, DigitalOcean, AWS, Heroku, etc… + +![Diagram showcasing how the Medusa server and its associated services would be deployed](https://res.cloudinary.com/dza7lstvk/image/upload/v1708600972/Medusa%20Book/backend_deployment_pgexo3.jpg) + +Your server connects to a PostgreSQL database, Redis, and other services relevant for your setup. Most hosting providers support deploying and managing these databases along with your Medusa server (such as Railway and DigitalOcean). + +When you deploy your Medusa application, you also deploy the Medusa Admin. For optimal experience, your hosting provider and plan must offer at least 2GB of RAM. + +### Deploy Server and Worker Instances + +By default, Medusa runs all processes in a single instance. This includes the server that handles incoming requests and the worker that processes background tasks. While this works for development, it’s not optimal for production environments as many background tasks can be long-running or resource-heavy. + +Instead, you should deploy two instances: + +- A server instance, which handles incoming requests to the application’s API routes. +- A worker instance, which processes background tasks, including scheduled jobs and subscribers. + +You don’t need to set up different projects for each instance. Instead, you can configure the Medusa application to run in different modes based on environment variables. + +Learn more about worker modes and how to configure them in the [Worker Mode chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +### How to Deploy Medusa? + +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. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +[Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) + +To self-host Medusa, the [next chapter](https://docs.medusajs.com/learn/deployment/general/index.html.md) explains the general steps to deploy your Medusa application. Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for general and specific hosting providers. + +*** + +## Deploying the Storefront + +The storefront is deployed separately from the Medusa application, and the hosting options depend on the tools and frameworks you use to create the storefront. + +If you’re using the Next.js Starter storefront, you may deploy the storefront to any hosting provider that supports frontend frameworks, such as Vercel. + +Per Vercel’s [license and plans](https://vercel.com/pricing), their free plan can only be used for personal, non-commercial projects. So, you can deploy the storefront on the free plan for development purposes, but for commercial projects, you must update your Vercel plan. + +Refer to [this reference](https://docs.medusajs.com/resources/deployment/index.html.md) to find how-to deployment guides for specific hosting providers. + + +# 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. + +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 In this chapter, you'll learn about updating your Medusa application and packages. @@ -451,28 +473,6 @@ npm 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. - -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). - - # Medusa Application Configuration In this chapter, you'll learn available configurations in the Medusa application. You can change the application's configurations to customize the behavior of the application, its integrated modules and plugins, and more. @@ -1376,314 +1376,6 @@ npx medusa db:migrate ``` -# General Medusa Application Deployment Guide - -In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. - -Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). - -Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) - -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. - -With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: - -- Push to deploy. -- Multiple testing environments. -- Preview environments for new PRs. -- Test on production-like data. - -### Prerequisites - -- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) - -## What You'll Deploy - -When you deploy the Medusa application, you need to deploy the following resources: - -1. PostgreSQL database: This is the database that will hold your Medusa application's details. -2. Redis database: This is the database that will store the Medusa server's session. -3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: - - The server mode handles incoming API requests and serving the Medusa Admin dashboard. - - The worker mode handles background tasks, such as scheduled jobs and subscribers. - -So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. - -*** - -## 1. Configure Medusa Application - -### Worker Mode - -The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. - -Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). - -So, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", - }, -}) -``` - -Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. - -### Configure Medusa Admin - -The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. - -To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - disable: process.env.DISABLE_MEDUSA_ADMIN === "true", - }, -}) -``` - -Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. - -### Configure Redis URL - -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/learn/configurations/medusa-config#redisurl/index.html.md). - -So, add the following configuration in `medusa-config.ts` : - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - // ... - redisUrl: process.env.REDIS_URL, - }, -}) -``` - -*** - -## 2. Add predeploy Script - -Before you start the Medusa application in production, you should always run migrations and sync links. - -So, add the following script in `package.json`: - -```json -"scripts": { - // ... - "predeploy": "medusa db:migrate" -}, -``` - -*** - -## 3. Install Production Modules and Providers - -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: - -- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) -- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) -- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) -- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). -- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). - -Then, add these modules in `medusa-config.ts`: - -```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, - }, - }, - }, - ], -}) -``` - -Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. - -*** - -## 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/). - -After hosting both databases, keep their connection URLs for the next steps. - -*** - -## 5. Deploy Medusa Application in Server Mode - -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 deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. - -### Set Environment Variables - -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 -``` - -Where: - -- 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`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm install && npm run predeploy && npm run start -``` - -Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. - -### Set Backend URL in Admin Configuration - -The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. - -After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - admin: { - // ... - backendUrl: process.env.MEDUSA_BACKEND_URL, - }, -}) -``` - -Then, push the changes to the GitHub repository or deployed application. - -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 -``` - -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`. - -After setting the environment variables, make sure to restart the deployment for the changes to take effect. - -Remember to separate URLs in `AUTH_CORS` by commas. - -*** - -## 6. Deploy Medusa Application in Worker Mode - -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. - -### Set Environment Variables - -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 -DISABLE_MEDUSA_ADMIN=true -MEDUSA_WORKER_MODE=worker -PORT=9000 -DATABASE_URL # POSTGRES DATABASE URL -REDIS_URL= # REDIS DATABASE URL -``` - -Where: - -- 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`. - -Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. - -### Set Start Command - -The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. - -If your hosting provider doesn't support setting a current-working directory, set the start command to the following: - -```bash npm2yarn -cd .medusa/server && npm run install && npm run start -``` - -*** - -## 7. Test Deployed Application - -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. - -The Medusa Admin is also available at `/app`. - -*** - -## 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. - - # Using TypeScript Aliases By default, Medusa doesn't support TypeScript aliases in production. @@ -2074,31 +1766,6 @@ 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. -# 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. - - # 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). @@ -2120,6 +1787,29 @@ In the next chapters, you'll continue with the brands example to: - Add a new page in the dashboard that shows all brands in the store. +# Integrate Third-Party Systems + +Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. + +The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. + +In Medusa, you integrate a third-party system by: + +1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. +2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. +3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. + +*** + +## Next Chapters: Sync Brands Example + +In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: + +1. Integrate a dummy third-party CMS in the Brand Module. +2. Sync brands to the CMS when a brand is created. +3. Sync brands from the CMS at a daily schedule. + + # Extend Core Commerce Features In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. @@ -2143,27 +1833,29 @@ The next chapters explain how to use the tools mentioned above with step-by-step - Retrieve a product's associated brand's details. -# Integrate Third-Party Systems +# Build Custom Features -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. +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. -The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. +By following these guides, you'll add brands to the Medusa application that you can associate with products. -In Medusa, you integrate a third-party system by: +To build a custom feature in Medusa, you need three main tools: -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. +- [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: Sync Brands Example +## Next Chapters: Brand Module 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: +The next chapters will guide you to: -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. +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 @@ -2378,85 +2070,6 @@ curl http://localhost:9000/hello-world You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. -# Environment Variables - -In this chapter, you'll learn how environment variables are loaded in Medusa. - -## System Environment Variables - -The Medusa application loads and uses system environment variables. - -For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. - -In production, you should always use system environment variables that you set through your hosting provider. - -*** - -## Environment Variables in .env Files - -During development, it's easier to set environment variables in a `.env` file in your repository. - -Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: - -As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. - -|\`.env\`| -|---|---| -|\`NODE\_ENV\`|\`.env\`| -|\`NODE\_ENV\`|\`.env.production\`| -|\`NODE\_ENV\`|\`.env.staging\`| -|\`NODE\_ENV\`|\`.env.test\`| - -### Set Environment in `loadEnv` - -In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. - -This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. - -To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. - -*** - -## Environment Variables for Admin Customizations - -Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. - -Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -*** - -## Predefined Medusa Environment Variables - -The Medusa application uses the following predefined environment variables that you can set: - -You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). - -|Environment Variable|Description|Default| -|---|---|---|---|---| -| -| -| -| -||The URL to connect to the PostgreSQL database. Only used if || -||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || -||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || -||URLs of clients that can access the Medusa backend's authentication routes. Only used if || -||A random string used to create authentication tokens in the http layer. Only used if || -||A random string used to create cookie tokens in the http layer. Only used if || -||The URL to the Medusa backend. Only used if || -| -| -| -| -| -| -| -| -||The allowed levels to log. Learn more in || -||The file to save logs in. By default, logs aren't saved in any file. Learn more in || -||Whether to disable analytics data collection. Learn more in || - - # Data Models In this chapter, you'll learn what a data model is and how to create a data model. @@ -2561,6 +2174,85 @@ For example, the Blog Module's service would have methods like `retrievePost` an Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. +# Environment Variables + +In this chapter, you'll learn how environment variables are loaded in Medusa. + +## System Environment Variables + +The Medusa application loads and uses system environment variables. + +For example, if you set the `PORT` environment variable to `8000`, the Medusa application runs on that port instead of `9000`. + +In production, you should always use system environment variables that you set through your hosting provider. + +*** + +## Environment Variables in .env Files + +During development, it's easier to set environment variables in a `.env` file in your repository. + +Based on your `NODE_ENV` system environment variable, Medusa will try to load environment variables from the following `.env` files: + +As of [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0), `NODE_ENV` defaults to `production` when using `medusa start`. Otherwise, it defaults to `development`. + +|\`.env\`| +|---|---| +|\`NODE\_ENV\`|\`.env\`| +|\`NODE\_ENV\`|\`.env.production\`| +|\`NODE\_ENV\`|\`.env.staging\`| +|\`NODE\_ENV\`|\`.env.test\`| + +### Set Environment in `loadEnv` + +In the `medusa-config.ts` file of your Medusa application, you'll find a `loadEnv` function used that accepts `process.env.NODE_ENV` as a first parameter. + +This function is responsible for loading the correct `.env` file based on the value of `process.env.NODE_ENV`. + +To ensure that the correct `.env` file is loaded as shown in the table above, only specify `development`, `production`, `staging` or `test` as the value of `process.env.NODE_ENV` or as the parameter of `loadEnv`. + +*** + +## Environment Variables for Admin Customizations + +Since the Medusa Admin is built on top of [Vite](https://vite.dev/), you prefix the environment variables you want to use in a widget or UI route with `VITE_`. Then, you can access or use them with the `import.meta.env` object. + +Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). + +*** + +## Predefined Medusa Environment Variables + +The Medusa application uses the following predefined environment variables that you can set: + +You should opt for setting configurations in `medusa-config.ts` where possible. For a full list of Medusa configurations, refer to the [Medusa Configurations chapter](https://docs.medusajs.com/learn/configurations/medusa-config/index.html.md). + +|Environment Variable|Description|Default| +|---|---|---|---|---| +| +| +| +| +||The URL to connect to the PostgreSQL database. Only used if || +||URLs of storefronts that can access the Medusa backend's Store APIs. Only used if || +||URLs of admin dashboards that can access the Medusa backend's Admin APIs. Only used if || +||URLs of clients that can access the Medusa backend's authentication routes. Only used if || +||A random string used to create authentication tokens in the http layer. Only used if || +||A random string used to create cookie tokens in the http layer. Only used if || +||The URL to the Medusa backend. Only used if || +| +| +| +| +| +| +| +| +||The allowed levels to log. Learn more in || +||The file to save logs in. By default, logs aren't saved in any file. Learn more in || +||Whether to disable analytics data collection. Learn more in || + + # Events and Subscribers In this chapter, you’ll learn about Medusa's event system, and how to handle events with subscribers. @@ -3583,44 +3275,6 @@ A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). -# Plugins - -In this chapter, you'll learn what a plugin is in Medusa. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## What is a Plugin? - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). - -Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. - -![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) - -Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). - -*** - -## Plugin vs Module - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. - -A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. - -For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. - -- You want to reuse your Medusa customizations across multiple projects. -- You want to share your Medusa customizations with the community. - -- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. - -*** - -## How to Create a Plugin? - -The next chapter explains how you can create and publish a plugin. - - # Define Module Link In this chapter, you’ll learn what a module link is and how to define one. @@ -3836,100 +3490,6 @@ npx medusa db:migrate ``` -# Scheduled Jobs - -In this chapter, you’ll learn about scheduled jobs and how to use them. - -## What is a Scheduled Job? - -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. - -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. - -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. - -- 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. - -*** - -## 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. - -For example, create the file `src/jobs/hello-world.ts` with the following content: - -![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" - -export default async function greetingJob(container: MedusaContainer) { - const logger = container.resolve("logger") - - logger.info("Greeting!") -} - -export const config = { - name: "greeting-every-minute", - schedule: "* * * * *", -} -``` - -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. - -You also export a `config` object that has the following properties: - -- `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!`. - -### Test the Scheduled Job - -To test out your scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -After a minute, the following message will be logged to the terminal: - -```bash -info: Greeting! -``` - -*** - -## Example: Sync Products Once a Day - -In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. - -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: - -```ts title="src/jobs/sync-products.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" - -export default async function syncProductsJob(container: MedusaContainer) { - await syncProductToErpWorkflow(container) - .run() -} - -export const config = { - name: "sync-products-job", - schedule: "0 0 * * *", -} -``` - -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 next time you start the Medusa application, it will run this job every day at midnight. - - # Modules In this chapter, you’ll learn about modules and how to create them. @@ -4230,6 +3790,44 @@ 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. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## What is a Plugin? + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. The supported customizations are [Modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), [API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md), [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md), [Subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md), [Scheduled Jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md), and [Admin Extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). + +Plugins allow you to reuse your Medusa customizations across multiple projects or share them with the community. They can be published to npm and installed in any Medusa project. + +![Diagram showcasing a wishlist plugin installed in a Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540762/Medusa%20Book/plugin-diagram_oepiis.jpg) + +Learn how to create a wishlist plugin in [this guide](https://docs.medusajs.com/resources/plugins/guides/wishlist/index.html.md). + +*** + +## Plugin vs Module + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) is an isolated package related to a single domain or functionality, such as product reviews or integrating a Content Management System. A module can't access any resources in the Medusa application that are outside its codebase. + +A plugin, on the other hand, can contain multiple Medusa customizations, including modules. Your plugin can define a module, then build flows around it. + +For example, in a plugin, you can define a module that integrates a third-party service, then add a workflow that uses the module when a certain event occurs to sync data to that service. + +- You want to reuse your Medusa customizations across multiple projects. +- You want to share your Medusa customizations with the community. + +- You want to build a custom feature related to a single domain or integrate a third-party service. Instead, use a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). You can wrap that module in a plugin if it's used in other customizations, such as if it has a module link or it's used in a workflow. + +*** + +## How to Create a Plugin? + +The next chapter explains how you can create and publish a plugin. + + # Medusa's Architecture In this chapter, you'll learn about the architectural layers in Medusa. @@ -4303,6 +3901,100 @@ The following diagram illustrates Medusa's architecture including all its layers ![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) +# Scheduled Jobs + +In this chapter, you’ll learn about scheduled jobs and how to use them. + +## What is a Scheduled Job? + +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. + +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. + +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. + +- 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. + +*** + +## 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. + +For example, create the file `src/jobs/hello-world.ts` with the following content: + +![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" + +export default async function greetingJob(container: MedusaContainer) { + const logger = container.resolve("logger") + + logger.info("Greeting!") +} + +export const config = { + name: "greeting-every-minute", + schedule: "* * * * *", +} +``` + +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. + +You also export a `config` object that has the following properties: + +- `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!`. + +### Test the Scheduled Job + +To test out your scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +After a minute, the following message will be logged to the terminal: + +```bash +info: Greeting! +``` + +*** + +## Example: Sync Products Once a Day + +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. + +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: + +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" + +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} + +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` + +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 next time you start the Medusa application, it will run this job every day at midnight. + + # Workflows In this chapter, you’ll learn about workflows and how to define and execute them. @@ -4557,6 +4249,314 @@ You can now execute this workflow in a custom API route, scheduled job, or subsc 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. +# General Medusa Application Deployment Guide + +In this document, you'll learn the general steps to deploy your Medusa application. How you apply these steps depend on your chosen hosting provider or platform. + +Find how-to guides for specific platforms in [this documentation](https://docs.medusajs.com/resources/deployment/index.html.md). + +Want Medusa to manage and maintain your infrastructure? [Sign up and learn more about Medusa Cloud](https://medusajs.com/pricing) + +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. + +With Medusa Cloud, you maintain full customization control as you deploy your own modules and customizations directly from GitHub: + +- Push to deploy. +- Multiple testing environments. +- Preview environments for new PRs. +- Test on production-like data. + +### Prerequisites + +- [Medusa application’s codebase hosted on GitHub repository.](https://docs.medusajs.com/learn/index.html.md) + +## What You'll Deploy + +When you deploy the Medusa application, you need to deploy the following resources: + +1. PostgreSQL database: This is the database that will hold your Medusa application's details. +2. Redis database: This is the database that will store the Medusa server's session. +3. Medusa application in [server and worker mode](https://docs.medusajs.com/learn/production/worker-mode/index.html.md), where: + - The server mode handles incoming API requests and serving the Medusa Admin dashboard. + - The worker mode handles background tasks, such as scheduled jobs and subscribers. + +So, when choosing a hosting provider, make sure it supports deploying these resources. Also, for optimal experience, the hosting provider and plan must offer at least 2GB of RAM. + +*** + +## 1. Configure Medusa Application + +### Worker Mode + +The `workerMode` configuration determines which mode the Medusa application runs in. When you deploy the Medusa application, you deploy two instances: one in server mode, and one in worker mode. + +Learn more about worker mode in the [Worker Module chapter](https://docs.medusajs.com/learn/production/worker-mode/index.html.md). + +So, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + workerMode: process.env.MEDUSA_WORKER_MODE as "shared" | "worker" | "server", + }, +}) +``` + +Later, you’ll set different values of the `MEDUSA_WORKER_MODE` environment variable for each Medusa application deployment or instance. + +### Configure Medusa Admin + +The Medusa Admin is served by the Medusa server application. So, you need to disable it in the worker Medusa application only. + +To disable the Medusa Admin in the worker Medusa application while keeping it enabled in the server Medusa application, add the following configuration in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + disable: process.env.DISABLE_MEDUSA_ADMIN === "true", + }, +}) +``` + +Later, you’ll set different values of the `DISABLE_MEDUSA_ADMIN` environment variable for each Medusa application instance. + +### Configure Redis URL + +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/learn/configurations/medusa-config#redisurl/index.html.md). + +So, add the following configuration in `medusa-config.ts` : + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + redisUrl: process.env.REDIS_URL, + }, +}) +``` + +*** + +## 2. Add predeploy Script + +Before you start the Medusa application in production, you should always run migrations and sync links. + +So, add the following script in `package.json`: + +```json +"scripts": { + // ... + "predeploy": "medusa db:migrate" +}, +``` + +*** + +## 3. Install Production Modules and Providers + +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: + +- [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md) +- [Redis Event Bus Module](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) +- [Workflow Engine Redis Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) +- [S3 File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) (or other file module providers production-ready). +- [SendGrid Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) (or other notification module providers production-ready). + +Then, add these modules in `medusa-config.ts`: + +```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, + }, + }, + }, + ], +}) +``` + +Check out the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) and [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) documentation for other modules and providers to use. + +*** + +## 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/). + +After hosting both databases, keep their connection URLs for the next steps. + +*** + +## 5. Deploy Medusa Application in Server Mode + +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 deployment steps depend on your hosting provider. This section provides the general steps to perform during the deployment. + +### Set Environment Variables + +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 +``` + +Where: + +- 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`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. If you're using environment variables in your admin customizations, make sure to set them as well, as they're inlined during the build process. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm install && npm run predeploy && npm run start +``` + +Notice that you run the `predeploy` command before starting the Medusa application to run migrations and sync links whenever there's an update. + +### Set Backend URL in Admin Configuration + +The Medusa Admin is built and hosted statically. To send requests to the Medusa server application, you must set the backend URL in the Medusa Admin's configuration. + +After you’ve obtained the Medusa application’s URL, add the following configuration to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + admin: { + // ... + backendUrl: process.env.MEDUSA_BACKEND_URL, + }, +}) +``` + +Then, push the changes to the GitHub repository or deployed application. + +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 +``` + +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`. + +After setting the environment variables, make sure to restart the deployment for the changes to take effect. + +Remember to separate URLs in `AUTH_CORS` by commas. + +*** + +## 6. Deploy Medusa Application in Worker Mode + +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. + +### Set Environment Variables + +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 +DISABLE_MEDUSA_ADMIN=true +MEDUSA_WORKER_MODE=worker +PORT=9000 +DATABASE_URL # POSTGRES DATABASE URL +REDIS_URL= # REDIS DATABASE URL +``` + +Where: + +- 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`. + +Feel free to add any other relevant environment variables, such as for integrations and Infrastructure Modules. + +### Set Start Command + +The Medusa application's production build, which is created using the `build` command, outputs the Medusa application to `.medusa/server`. So, you must install the dependencies in the `.medusa/server` directory, then run the `start` command in it. + +If your hosting provider doesn't support setting a current-working directory, set the start command to the following: + +```bash npm2yarn +cd .medusa/server && npm run install && npm run start +``` + +*** + +## 7. Test Deployed Application + +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. + +The Medusa Admin is also available at `/app`. + +*** + +## 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. + + # Worker Mode of Medusa Instance In this chapter, you'll learn about the different modes of running a Medusa instance and how to configure the mode. @@ -4821,214 +4821,6 @@ To manage that database, such as changing its name or perform operations on it i The next chapters provide examples of writing integration tests for API routes and workflows. -# 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. - -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. - -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. - -### Prerequisites - -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) - -## 1. Create the API Route - -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…). - -Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). - -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: - -![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) - -```ts title="src/api/admin/brands/route.ts" -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { - createBrandWorkflow, -} from "../../../workflows/create-brand" - -type PostAdminCreateBrandType = { - name: string -} - -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, - }) - - res.json({ brand: result }) -} -``` - -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. - -`MedusaRequest` accepts the request body's type as a type argument. - -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`. - -You return a JSON response with the created brand using the `res.json` method. - -*** - -## 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. - -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. - -Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). - -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: - -![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) - -```ts title="src/api/admin/brands/validators.ts" -import { z } from "zod" - -export const PostAdminCreateBrand = z.object({ - name: z.string(), -}) -``` - -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: - -```ts title="src/api/admin/brands/route.ts" -// ... -import { z } from "zod" -import { PostAdminCreateBrand } from "./validators" - -type PostAdminCreateBrandType = z.infer - -// ... -``` - -*** - -## 3. Add Validation Middleware - -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). - -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: - -![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) - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { PostAdminCreateBrand } from "./admin/brands/validators" - -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. - -In the middleware object, you define three properties: - -- `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. - -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. - -*** - -## Test API Route - -To test out the API route, start the Medusa application with the following command: - -```bash npm2yarn -npm run dev -``` - -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. - -So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: - -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` - -This returns the created brand in the response: - -```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" - } -} -``` - -*** - -## Summary - -By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: - -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 Steps: Associate Brand with Product - -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 the next chapters, you'll learn how to build associations between data models defined in different modules. - - # Write Tests for Modules In this chapter, you'll learn about `moduleIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests for a module's main service. @@ -5148,300 +4940,158 @@ The `moduleIntegrationTestRunner` function creates a database with a random name To manage that database, such as changing its name or perform operations on it in your tests, refer to [the Test Tooling Reference](https://docs.medusajs.com/resources/test-tools-reference/moduleIntegrationTestRunner/index.html.md). -# Guide: Create Brand Workflow +# Guide: Add Product's Brand Widget in Admin -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. - -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. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). +In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. ### Prerequisites -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) -*** +## 1. Initialize JS SDK -## 1. Create createBrandStep +In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. -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 +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: -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 directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) -![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) +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" -```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" - -export type CreateBrandStepInput = { - name: string -} - -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 `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. - -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. - -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. - -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. - -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). - -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. - -### Add Compensation Function to Step - -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. - -Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: - -```ts title="src/workflows/create-brand.ts" -export const createBrandStep = createStep( - // ... - async (id: string, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.deleteBrands(id) - } -) -``` - -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. - -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. - -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). - -So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. - -*** - -## 2. Create createBrandWorkflow - -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. - -Add the following content in the same `src/workflows/create-brand.ts` file: - -```ts title="src/workflows/create-brand.ts" -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -type CreateBrandWorkflowInput = { - name: string -} - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandWorkflowInput) => { - const brand = createBrandStep(input) - - return new WorkflowResponse(brand) - } -) -``` - -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. - -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. - -A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. - -*** - -## Next Steps: Expose Create Brand API Route - -You now have a `createBrandWorkflow` that you can execute to create a brand. - -In the next chapter, you'll add an API route that allows admin users to create a brand. You'll learn how to create the API route, and execute in it the workflow you implemented in this chapter. - - -# Guide: Implement Brand Module - -In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. - -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. - -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. - -![Diagram showcasing an overview of the Brand Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1746546820/Medusa%20Resources/brand-module_pg86gm.jpg) - -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -## 1. Create Module Directory - -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. - -![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) - -*** - -## 2. Create Data Model - -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). - -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: - -![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) - -```ts title="src/modules/brand/models/brand.ts" -import { model } from "@medusajs/framework/utils" - -export const Brand = model.define("brand", { - id: model.id().primaryKey(), - name: model.text(), +export const sdk = new Medusa({ + baseUrl: import.meta.env.VITE_BACKEND_URL || "/", + debug: import.meta.env.DEV, + auth: { + type: "session", + }, }) ``` -You create a `Brand` data model which has an `id` primary key property, and a `name` text property. +You initialize the SDK passing it the following options: -You define the data model using the `define` method of the DML. It accepts two parameters: +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. -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. +Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). -Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md). +You can now use the SDK to send requests to the Medusa server. + +Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). *** -## 3. Create Module Service +## 2. Add Widget to Product Details Page -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. +You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. -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 widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). -Learn more about services in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#2-create-service/index.html.md). +To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: -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: +![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) -![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) - -```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} -import { MedusaService } from "@medusajs/framework/utils" -import { Brand } from "./models/brand" - -class BrandModuleService extends MedusaService({ - Brand, -}) { +```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" +import { clx, Container, Heading, Text } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string + } } -export default BrandModuleService -``` +const ProductBrandWidget = ({ + data: product, +}: DetailWidgetProps) => { + const { data: queryResult } = useQuery({ + queryFn: () => sdk.admin.product.retrieve(product.id, { + fields: "+brand.*", + }), + queryKey: [["product", product.id]], + }) + const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name -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. + return ( + +
+
+ Brand +
+
+
+ + Name + -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. + + {brandName || "-"} + +
+
+ ) +} -You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). - -Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). - -*** - -## 4. Export Module Definition - -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: - -![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) - -```ts title="src/modules/brand/index.ts" -import { Module } from "@medusajs/framework/utils" -import BrandModuleService from "./service" - -export const BRAND_MODULE = "brand" - -export default Module(BRAND_MODULE, { - service: BrandModuleService, +export const config = defineWidgetConfig({ + zone: "product.details.before", }) + +export default ProductBrandWidget ``` -You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: +A widget's file must export: -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. +- A React component to be rendered in the specified injection zone. The component must be the file's default export. +- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. -You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. +Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. + +In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. *** -## 5. Add Module to Medusa's Configurations +## Test it Out -To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. +To test out your widget, start the Medusa application: -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`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/brand", - }, - ], -}) +```bash npm2yarn +npm run dev ``` -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). +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. + +![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) *** -## 6. Generate and Run Migrations +## Admin Components Guides -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. +When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. -Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). - -[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: - -```bash -npx medusa db:generate brand -npx medusa db:migrate -``` - -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. +The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. *** -## Next Step: Create Brand Workflow +## Next Chapter: Add UI Route for Brands -The Brand Module now creates a `brand` table in the database and provides a class to manage its records. - -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. +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. # Create Brands UI Route in Admin @@ -5816,442 +5466,6 @@ Your customizations often span across systems, where you need to retrieve data o In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. -# Guide: Add Product's Brand Widget in Admin - -In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. - -### Prerequisites - -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -## 1. Initialize JS SDK - -In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. - -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: - -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) - -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -You initialize the SDK passing it the following options: - -- `baseUrl`: The URL to the Medusa server. -- `debug`: Whether to enable logging debug messages. This should only be enabled in development. -- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. - -Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -You can now use the SDK to send requests to the Medusa server. - -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). - -*** - -## 2. Add Widget to Product Details Page - -You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. - -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). - -To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: - -![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) - -```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" -import { clx, Container, Heading, Text } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/sdk" - -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string - } -} - -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps) => { - const { data: queryResult } = useQuery({ - queryFn: () => sdk.admin.product.retrieve(product.id, { - fields: "+brand.*", - }), - queryKey: [["product", product.id]], - }) - const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - - return ( - -
-
- Brand -
-
-
- - Name - - - - {brandName || "-"} - -
-
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductBrandWidget -``` - -A widget's file must export: - -- A React component to be rendered in the specified injection zone. The component must be the file's default export. -- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. - -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. - -In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. - -*** - -## Test it Out - -To test out your widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. - -![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) - -*** - -## Admin Components Guides - -When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. - -The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. - -*** - -## Next Chapter: Add UI Route for Brands - -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. - - -# Guide: Define Module Link Between Brand and Product - -In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. - -Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. - -A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. - -In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. - -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -### Prerequisites - -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -## 1. Define Link - -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. - -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: - -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - -```ts title="src/links/product-brand.ts" highlights={highlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. - -The `defineLink` function accepts two parameters of the same type, which is either: - -- The data model's link configuration, which you access from the Module's `linkable` property; -- Or an object that has two properties: - - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. - - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. - -So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. - -*** - -## 2. Sync the Link to the Database - -A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: - -```bash -npx medusa db:migrate -``` - -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. - -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. - -*** - -## Next Steps: Extend Create Product Flow - -In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. - - -# Guide: Extend Create Product Flow - -After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. - -Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. - -So, in this chapter, to extend the create product flow and associate a brand with a product, you will: - -- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. -- Extend the Create Product API route to allow passing a brand ID in `additional_data`. - -To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -*** - -## 1. Consume the productsCreated Hook - -A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. - -Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - -The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters. - -To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: - -![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) - -```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" -import { StepResponse } from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { LinkDefinition } from "@medusajs/framework/types" -import { BRAND_MODULE } from "../../modules/brand" -import BrandModuleService from "../../modules/brand/service" - -createProductsWorkflow.hooks.productsCreated( - (async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand_id) { - return new StepResponse([], []) - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - // if the brand doesn't exist, an error is thrown. - await brandModuleService.retrieveBrand(additional_data.brand_id as string) - - // TODO link brand to product - }) -) -``` - -Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: - -1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. -2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools. - -In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. - -### Link Brand to Product - -Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. - -Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). - -To use Link in the `productsCreated` hook, replace the `TODO` with the following: - -```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} -const link = container.resolve("link") -const logger = container.resolve("logger") - -const links: LinkDefinition[] = [] - -for (const product of products) { - links.push({ - [Modules.PRODUCT]: { - product_id: product.id, - }, - [BRAND_MODULE]: { - brand_id: additional_data.brand_id, - }, - }) -} - -await link.create(links) - -logger.info("Linked brand to products") - -return new StepResponse(links, links) -``` - -You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. - -Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. - -![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) - -Finally, you return an instance of `StepResponse` returning the created links. - -### Dismiss Links in Compensation - -You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. - -To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: - -```ts title="src/workflows/hooks/created-product.ts" -createProductsWorkflow.hooks.productsCreated( - // ... - (async (links, { container }) => { - if (!links?.length) { - return - } - - const link = container.resolve("link") - - await link.dismiss(links) - }) -) -``` - -In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. - -*** - -## 2. Configure Additional Data Validation - -Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. - -You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: - -![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) - -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" - -// ... - -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/products", - method: ["POST"], - additionalDataValidator: { - brand_id: z.string().optional(), - }, - }, - ], -}) -``` - -Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). - -So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. - -*** - -## Test it Out - -To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password in the request body with your user's credentials. - -Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: - -```bash -curl -X POST 'http://localhost:9000/admin/products' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "title": "Product 1", - "options": [ - { - "title": "Default option", - "values": ["Default option value"] - } - ], - "shipping_profile_id": "{shipping_profile_id}", - "additional_data": { - "brand_id": "{brand_id}" - } -}' -``` - -Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). - -The request creates a product and returns it. - -In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products. - -*** - -## Next Steps: Query Linked Brands and Products - -Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. - - # Guide: Sync Brands from Medusa to Third-Party In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. @@ -6524,152 +5738,233 @@ info: API Key: "123" You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. -# Guide: Query Product's Brands +# Guide: Integrate Third-Party Brand System -In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. +In 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. -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. +Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -### Prerequisites +## 1. Create Module Directory -- [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) +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) *** -## Approach 1: Retrieve Brands in Existing API Routes +## 2. Create Module Service -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. +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. -Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: -For example, send the following request to retrieve the list of products with their brands: +![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) -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" + +export type ModuleOptions = { + apiKey: string +} + +type InjectedDependencies = { + logger: Logger + configModule: ConfigModule +} + +class CmsModuleService { + private options_: ModuleOptions + private logger_: Logger + + constructor({ logger }: InjectedDependencies, options: ModuleOptions) { + this.logger_ = logger + this.options_ = options + + // TODO initialize SDK + } +} + +export default CmsModuleService ``` -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). +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: -Any product that is linked to a brand will have a `brand` property in its object: +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. -```json title="Example Product Object" -{ - "id": "prod_123", +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. + +### Integration Methods + +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. + +Add the following methods in the `CmsModuleService`: + +```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} +export class CmsModuleService { // ... - "brand": { - "id": "01JEB44M61BRM3ARM2RRMK7GJF", - "name": "Acme", - "created_at": "2024-12-05T09:59:08.737Z", - "updated_at": "2024-12-05T09:59:08.737Z", - "deleted_at": null + + // 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)}`) + } + + async createBrand(brand: Record) { + await this.sendRequest("/brands", "POST", brand) + } + + async deleteBrand(id: string) { + await this.sendRequest(`/brands/${id}`, "DELETE") + } + + async retrieveBrands(): Promise[]> { + await this.sendRequest("/brands", "GET") + + return [] } } ``` -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 `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. -### Limitations: Filtering by Brands in Existing API Routes +You also add three methods that use the `sendRequest` method: -While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. - -Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). +- `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. *** -## Approach 2: Use Query to Retrieve Linked Records +## 3. Export Module Definition -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. +After creating the module's service, you'll export the module definition indicating the module's name and service. -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). +Create the file `src/modules/cms/index.ts` with the following content: -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: +![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/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" -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 const CMS_MODULE = "cms" - res.json({ brands }) -} +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) ``` -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: +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`. -- `entity`: The data model's name as specified in the first parameter of `model.define`. -- `fields`: An array of properties and relations to retrieve. You can pass: - - A property's name, such as `id`, or `*` for all properties. - - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. +*** -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. +## 4. Add Module to Medusa's Configurations -### Test it Out +Finally, add the module to the Medusa configurations at `medusa-config.ts`: -To test the API route out, send a `GET` request to `/admin/brands`: +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... + { + resolve: "./src/modules/cms", + options: { + apiKey: process.env.CMS_API_KEY, + }, + }, + ], +}) +``` + +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 can add the `CMS_API_KEY` environment variable to `.env`: ```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' +CMS_API_KEY=123 ``` -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -This returns the brands in your store with their linked products. For example: - -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` - -### Limitations: Filtering by Brand in Query - -While you can use Query to retrieve linked records, you can't filter by linked records. - -For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - *** -## Summary +## Next Steps: Sync Brand From Medusa to CMS -By following the examples of the previous chapters, you: +You can now use the CMS Module's service to perform actions on the third-party CMS. -- 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. +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. + + +# Guide: Define Module Link Between Brand and Product + +In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. + +Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. + +A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. + +In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. + +Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). + +### Prerequisites + +- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +## 1. Define Link + +Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. + +So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: + +![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) + +```ts title="src/links/product-brand.ts" highlights={highlights} +import BrandModule from "../modules/brand" +import ProductModule from "@medusajs/medusa/product" +import { defineLink } from "@medusajs/framework/utils" + +export default defineLink( + { + linkable: ProductModule.linkable.product, + isList: true, + }, + BrandModule.linkable.brand +) +``` + +You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. + +The `defineLink` function accepts two parameters of the same type, which is either: + +- The data model's link configuration, which you access from the Module's `linkable` property; +- Or an object that has two properties: + - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. + - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. + +So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. *** -## Next Steps: Customize Medusa Admin +## 2. Sync the Link to the Database -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. +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: -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. +```bash +npx medusa db:migrate +``` + +This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. + +You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. + +*** + +## Next Steps: Extend Create Product Flow + +In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. # Guide: Schedule Syncing Brands from Third-Party @@ -6981,285 +6276,868 @@ By following the previous chapters, you utilized the Medusa Framework and orches 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. -# Guide: Integrate Third-Party Brand System +# Guide: Extend Create Product Flow -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. +After linking the [custom Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) in the [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md), you'll extend the create product workflow and API route to allow associating a brand with a product. + +Some API routes, including the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. + +So, in this chapter, to extend the create product flow and associate a brand with a product, you will: + +- Consume the [productsCreated](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow#productsCreated/index.html.md) hook of the [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md), which is executed within the workflow after the product is created. You'll link the product with the brand passed in the `additional_data` parameter. +- Extend the Create Product API route to allow passing a brand ID in `additional_data`. + +To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +*** + +## 1. Consume the productsCreated Hook + +A workflow hook is a point in a workflow where you can inject a step to perform a custom functionality. Consuming a workflow hook allows you to extend the features of a workflow and, consequently, the API route that uses it. + +Learn more about the workflow hooks in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). + +The [createProductsWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) used in the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts) has a `productsCreated` hook that runs after the product is created. You'll consume this hook to link the created product with the brand specified in the request parameters. + +To consume the `productsCreated` hook, create the file `src/workflows/hooks/created-product.ts` with the following content: + +![Directory structure after creating the hook's file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733384338/Medusa%20Book/brands-hook-dir-overview_ltwr5h.jpg) + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook1Highlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +import { StepResponse } from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { LinkDefinition } from "@medusajs/framework/types" +import { BRAND_MODULE } from "../../modules/brand" +import BrandModuleService from "../../modules/brand/service" + +createProductsWorkflow.hooks.productsCreated( + (async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand_id) { + return new StepResponse([], []) + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + // if the brand doesn't exist, an error is thrown. + await brandModuleService.retrieveBrand(additional_data.brand_id as string) + + // TODO link brand to product + }) +) +``` + +Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productsCreated`, accepts a step function as a parameter. The step function accepts the following parameters: + +1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. +2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) to resolve Framework and commerce tools. + +In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. + +### Link Brand to Product + +Next, you want to create a link between the created products and the brand. To do so, you use Link, which is a class from the Modules SDK that provides methods to manage linked records. + +Learn more about Link in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/link/index.html.md). + +To use Link in the `productsCreated` hook, replace the `TODO` with the following: + +```ts title="src/workflows/hooks/created-product.ts" highlights={hook2Highlights} +const link = container.resolve("link") +const logger = container.resolve("logger") + +const links: LinkDefinition[] = [] + +for (const product of products) { + links.push({ + [Modules.PRODUCT]: { + product_id: product.id, + }, + [BRAND_MODULE]: { + brand_id: additional_data.brand_id, + }, + }) +} + +await link.create(links) + +logger.info("Linked brand to products") + +return new StepResponse(links, links) +``` + +You resolve Link from the container. Then you loop over the created products to assemble an array of links to be created. After that, you pass the array of links to Link's `create` method, which will link the product and brand records. + +Each property in the link object is the name of a module, and its value is an object having a `{model_name}_id` property, where `{model_name}` is the snake-case name of the module's data model. Its value is the ID of the record to be linked. The link object's properties must be set in the same order as the link configurations passed to `defineLink`. + +![Diagram showcasing how the order of defining a link affects creating the link](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386156/Medusa%20Book/remote-link-brand-product-exp_fhjmg4.jpg) + +Finally, you return an instance of `StepResponse` returning the created links. + +### Dismiss Links in Compensation + +You can pass as a second parameter of the hook a compensation function that undoes what the step did. It receives as a first parameter the returned `StepResponse`'s second parameter, and the step context object as a second parameter. + +To undo creating the links in the hook, pass the following compensation function as a second parameter to `productsCreated`: + +```ts title="src/workflows/hooks/created-product.ts" +createProductsWorkflow.hooks.productsCreated( + // ... + (async (links, { container }) => { + if (!links?.length) { + return + } + + const link = container.resolve("link") + + await link.dismiss(links) + }) +) +``` + +In the compensation function, if the `links` parameter isn't empty, you resolve Link from the container and use its `dismiss` method. This method removes a link between two records. It accepts the same parameter as the `create` method. + +*** + +## 2. Configure Additional Data Validation + +Now that you've consumed the `productsCreated` hook, you want to configure the `/admin/products` API route that creates a new product to accept a brand ID in its `additional_data` parameter. + +You configure the properties accepted in `additional_data` in the `src/api/middlewares.ts` that exports middleware configurations. So, create the file (or, if already existing, add to the file) `src/api/middlewares.ts` the following content: + +![Directory structure after adding the middelwares file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733386868/Medusa%20Book/brands-middleware-dir-overview_uczos1.jpg) + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +// ... + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/products", + method: ["POST"], + additionalDataValidator: { + brand_id: z.string().optional(), + }, + }, + ], +}) +``` + +Objects in `routes` accept an `additionalDataValidator` property that configures the validation rules for custom properties passed in the `additional_data` request parameter. It accepts an object whose keys are custom property names, and their values are validation rules created using [Zod](https://zod.dev/). + +So, `POST` requests sent to `/admin/products` can now pass the ID of a brand in the `brand_id` property of `additional_data`. + +*** + +## Test it Out + +To test it out, first, retrieve the authentication token of your admin user by sending a `POST` request to `/auth/user/emailpass`: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password in the request body with your user's credentials. + +Then, send a `POST` request to `/admin/products` to create a product, and pass in the `additional_data` parameter a brand's ID: + +```bash +curl -X POST 'http://localhost:9000/admin/products' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "title": "Product 1", + "options": [ + { + "title": "Default option", + "values": ["Default option value"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand_id": "{brand_id}" + } +}' +``` + +Make sure to replace `{token}` with the token you received from the previous request, `shipping_profile_id` with the ID of a shipping profile in your application, and `{brand_id}` with the ID of a brand in your application. You can retrieve the ID of a shipping profile either from the Medusa Admin, or the [List Shipping Profiles API route](https://docs.medusajs.com/api/admin#shipping-profiles_getshippingprofiles). + +The request creates a product and returns it. + +In the Medusa application's logs, you'll find the message `Linked brand to products`, indicating that the workflow hook handler ran and linked the brand to the products. + +*** + +## Next Steps: Query Linked Brands and Products + +Now that you've extending the create-product flow to link a brand to it, you want to retrieve the brand details of a product. You'll learn how to do so in the next chapter. + + +# Guide: 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. + +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. + +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. + +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) + +## 1. Create the API Route + +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…). + +Learn more about API routes [in this guide](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md). + +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: + +![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) + +```ts title="src/api/admin/brands/route.ts" +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { + createBrandWorkflow, +} from "../../../workflows/create-brand" + +type PostAdminCreateBrandType = { + name: string +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const { result } = await createBrandWorkflow(req.scope) + .run({ + input: req.validatedBody, + }) + + res.json({ brand: result }) +} +``` + +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. + +`MedusaRequest` accepts the request body's type as a type argument. + +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`. + +You return a JSON response with the created brand using the `res.json` method. + +*** + +## 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. + +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. + +Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). + +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: + +![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) + +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" + +export const PostAdminCreateBrand = z.object({ + name: z.string(), +}) +``` + +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: + +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" + +type PostAdminCreateBrandType = z.infer + +// ... +``` + +*** + +## 3. Add Validation Middleware + +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). + +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: + +![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) + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" + +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. + +In the middleware object, you define three properties: + +- `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. + +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. + +*** + +## Test API Route + +To test out the API route, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. + +So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This returns the created brand in the response: + +```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" + } +} +``` + +*** + +## Summary + +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: + +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 Steps: Associate Brand with Product + +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 the next chapters, you'll learn how to build associations between data models defined in different modules. + + +# Guide: Implement Brand Module + +In this chapter, you'll build a Brand Module that adds a `brand` table to the database and provides data-management features for it. + +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. + +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. + +![Diagram showcasing an overview of the Brand Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1746546820/Medusa%20Resources/brand-module_pg86gm.jpg) Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). ## 1. Create Module Directory -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. +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. -![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) +![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) *** -## 2. Create Module Service +## 2. Create Data Model -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. +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. -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: +Learn more about data models in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#1-create-data-model/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) +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: -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" +![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) -export type ModuleOptions = { - apiKey: string -} +```ts title="src/modules/brand/models/brand.ts" +import { model } from "@medusajs/framework/utils" -type InjectedDependencies = { - logger: Logger - configModule: ConfigModule -} - -class CmsModuleService { - private options_: ModuleOptions - private logger_: Logger - - constructor({ logger }: InjectedDependencies, options: ModuleOptions) { - this.logger_ = logger - this.options_ = options - - // TODO initialize SDK - } -} - -export default CmsModuleService -``` - -You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: - -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. - -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. - -### Integration Methods - -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. - -Add the following methods in the `CmsModuleService`: - -```ts title="src/modules/cms/service.ts" highlights={methodsHighlights} -export class CmsModuleService { - // ... - - // 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)}`) - } - - async createBrand(brand: Record) { - await this.sendRequest("/brands", "POST", brand) - } - - async deleteBrand(id: string) { - await this.sendRequest(`/brands/${id}`, "DELETE") - } - - async retrieveBrands(): Promise[]> { - await this.sendRequest("/brands", "GET") - - return [] - } -} -``` - -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. - -*** - -## 3. Export Module Definition - -After creating the module's service, you'll export the module definition indicating the module's name and service. - -Create the file `src/modules/cms/index.ts` with the following content: - -![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" - -export const CMS_MODULE = "cms" - -export default Module(CMS_MODULE, { - service: CmsModuleService, +export const Brand = model.define("brand", { + id: model.id().primaryKey(), + name: model.text(), }) ``` -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`. +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: + +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. + +Learn about other property types in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties/index.html.md). *** -## 4. Add Module to Medusa's Configurations +## 3. Create Module Service -Finally, add the module to the Medusa configurations at `medusa-config.ts`: +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. + +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 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: + +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) + +```ts title="src/modules/brand/service.ts" highlights={serviceHighlights} +import { MedusaService } from "@medusajs/framework/utils" +import { Brand } from "./models/brand" + +class BrandModuleService extends MedusaService({ + Brand, +}) { + +} + +export default BrandModuleService +``` + +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. + +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. + +You'll use these methods in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). + +Find a reference of all generated methods in [this guide](https://docs.medusajs.com/resources/service-factory-reference/index.html.md). + +*** + +## 4. Export Module Definition + +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: + +![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) + +```ts title="src/modules/brand/index.ts" +import { Module } from "@medusajs/framework/utils" +import BrandModuleService from "./service" + +export const BRAND_MODULE = "brand" + +export default Module(BRAND_MODULE, { + service: BrandModuleService, +}) +``` + +You use `Module` from the Modules SDK to create the module's definition. It accepts two parameters: + +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. + +You export `BRAND_MODULE` to reference the module's name more reliably in other customizations. + +*** + +## 5. Add Module to Medusa's Configurations + +To start using your module, you must add it to Medusa's configurations in `medusa-config.ts`. + +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`: ```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ - // ... { - resolve: "./src/modules/cms", - options: { - apiKey: process.env.CMS_API_KEY, - }, + resolve: "./src/modules/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. +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). -You can add the `CMS_API_KEY` environment variable to `.env`: +*** + +## 6. Generate and Run Migrations + +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. + +Learn more about migrations in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules#5-generate-migrations/index.html.md). + +[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: ```bash -CMS_API_KEY=123 +npx medusa db:generate brand +npx medusa db:migrate ``` +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. + *** -## Next Steps: Sync Brand From Medusa to CMS +## Next Step: Create Brand Workflow -You can now use the CMS Module's service to perform actions on the third-party CMS. +The Brand Module now creates a `brand` table in the database and provides a class to manage its records. -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 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. -# Admin Development Constraints +# Guide: Query Product's Brands -This chapter lists some constraints of admin widgets and UI routes. +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. -## Arrow Functions +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. -Widget and UI route components must be created as arrow functions. +### Prerequisites -```ts highlights={arrowHighlights} -// Don't -function ProductWidget() { +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +*** + +## Approach 1: Retrieve Brands in Existing API Routes + +Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. + +Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. + +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +Any product that is linked to a brand will have a `brand` property in its object: + +```json title="Example Product Object" +{ + "id": "prod_123", // ... + "brand": { + "id": "01JEB44M61BRM3ARM2RRMK7GJF", + "name": "Acme", + "created_at": "2024-12-05T09:59:08.737Z", + "updated_at": "2024-12-05T09:59:08.737Z", + "deleted_at": null + } +} +``` + +By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. + +### Limitations: Filtering by Brands in Existing API Routes + +While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. + +Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Approach 2: Use Query to Retrieve Linked Records + +You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. + +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: + +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) + + res.json({ brands }) +} +``` + +This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: + +- `entity`: The data model's name as specified in the first parameter of `model.define`. +- `fields`: An array of properties and relations to retrieve. You can pass: + - A property's name, such as `id`, or `*` for all properties. + - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. + +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. + +### Test it Out + +To test the API route out, send a `GET` request to `/admin/brands`: + +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +This returns the brands in your store with their linked products. For example: + +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` + +### Limitations: Filtering by Brand in Query + +While you can use Query to retrieve linked records, you can't filter by linked records. + +For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Summary + +By following the examples of the previous chapters, you: + +- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. +- Extended the create-product workflow and route to allow setting the product's brand while creating the product. +- Queried a product's brand, and vice versa. + +*** + +## Next Steps: Customize Medusa Admin + +Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. + +In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. + + +# Guide: Create Brand Workflow + +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. + +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. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) + +*** + +## 1. Create createBrandStep + +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 + +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: + +![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) + +```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" + +export type CreateBrandStepInput = { + name: string } -// Do -const ProductWidget = () => { +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 `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. + +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. + +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. + +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. + +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). + +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. + +### Add Compensation Function to Step + +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. + +Learn more about the compensation function in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: + +```ts title="src/workflows/create-brand.ts" +export const createBrandStep = createStep( // ... -} + async (id: string, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(id) + } +) ``` +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. + +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. + +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). + +So, if an error occurs during the workflow's execution, the brand that was created by the step is deleted to maintain data consistency. + *** -## Widget Zone +## 2. Create createBrandWorkflow -A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. +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. -```ts highlights={zoneHighlights} -// Don't -export const config = defineWidgetConfig({ - zone: `product.details.before`, -}) +Add the following content in the same `src/workflows/create-brand.ts` file: -// Don't -const ZONE = "product.details.after" -export const config = defineWidgetConfig({ - zone: ZONE, -}) +```ts title="src/workflows/create-brand.ts" +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" -// Do -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) -``` +// ... - -# Environment Variables in Admin Customizations - -In this chapter, you'll learn how to use environment variables in your admin customizations. - -To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). - -## How to Set Environment Variables - -The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. - -For example: - -```plain -VITE_MY_API_KEY=sk_123 -``` - -*** - -## How to Use Environment Variables - -To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. - -For example: - -```tsx highlights={[["8"]]} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -const ProductWidget = () => { - return ( - -
- API Key: {import.meta.env.VITE_MY_API_KEY} -
-
- ) +type CreateBrandWorkflowInput = { + name: string } -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandWorkflowInput) => { + const brand = createBrandStep(input) -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`. +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. -### Type Error on import.meta.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. -If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: - -```ts title="src/admin/vite-env.d.ts" -/// -``` - -This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. +A workflow must return an instance of `WorkflowResponse`. It accepts as a parameter the data to return to the workflow's executor. *** -## Check Node Environment in Admin Customizations +## Next Steps: Expose Create Brand API Route -To check the current environment, Vite exposes two variables: +You now have a `createBrandWorkflow` that you can execute to create a brand. -- `import.meta.env.DEV`: Returns `true` if the current environment is development. -- `import.meta.env.PROD`: Returns `true` if the current environment is production. - -Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). - -*** - -## Environment Variables in Production - -When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application. - -For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. +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. # Admin Routing Customizations @@ -7415,6 +7293,51 @@ export const handle = { Refer to [react-router-dom’s documentation](https://reactrouter.com/en/6.29.0) for components and hooks that you can use in your admin customizations. +# Admin Development Constraints + +This chapter lists some constraints of admin widgets and UI routes. + +## Arrow Functions + +Widget and UI route components must be created as arrow functions. + +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... +} + +// Do +const ProductWidget = () => { + // ... +} +``` + +*** + +## Widget Zone + +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. + +```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, +}) + +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) +``` + + # Admin Development Tips In this chapter, you'll find some tips for your admin development. @@ -7546,6 +7469,83 @@ The Medusa Admin dashboard can be displayed in languages other than English, whi Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). +# Environment Variables in Admin Customizations + +In this chapter, you'll learn how to use environment variables in your admin customizations. + +To learn how environment variables are generally loaded in Medusa based on your application's environment, check out [this chapter](https://docs.medusajs.com/learn/fundamentals/environment-variables/index.html.md). + +## How to Set Environment Variables + +The Medusa Admin is built on top of [Vite](https://vite.dev/). To set an environment variable that you want to use in a widget or UI route, prefix the environment variable with `VITE_`. + +For example: + +```plain +VITE_MY_API_KEY=sk_123 +``` + +*** + +## How to Use Environment Variables + +To access or use an environment variable starting with `VITE_`, use the `import.meta.env` object. + +For example: + +```tsx highlights={[["8"]]} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" + +const ProductWidget = () => { + return ( + +
+ API Key: {import.meta.env.VITE_MY_API_KEY} +
+
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +In this example, you display the API key in a widget using `import.meta.env.VITE_MY_API_KEY`. + +### Type Error on import.meta.env + +If you receive a type error on `import.meta.env`, create the file `src/admin/vite-env.d.ts` with the following content: + +```ts title="src/admin/vite-env.d.ts" +/// +``` + +This file tells TypeScript to recognize the `import.meta.env` object and enhances the types of your custom environment variables. + +*** + +## Check Node Environment in Admin Customizations + +To check the current environment, Vite exposes two variables: + +- `import.meta.env.DEV`: Returns `true` if the current environment is development. +- `import.meta.env.PROD`: Returns `true` if the current environment is production. + +Learn more about other Vite environment variables in the [Vite documentation](https://vite.dev/guide/env-and-mode). + +*** + +## Environment Variables in Production + +When you build the Medusa application, including the Medusa Admin, with the `build` command, the environment variables are inlined into the build. This means that you can't change the environment variables without rebuilding the application. + +For example, the `VITE_MY_API_KEY` environment variable in the example above will be replaced with the actual value during the build process. + + # Admin UI Routes In this chapter, you’ll learn how to create a UI route in the admin dashboard. @@ -7901,6 +7901,205 @@ Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injec To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. +# Pass Additional Data to Medusa's API Route + +In this chapter, you'll learn how to pass additional data in requests to Medusa's API Route. + +## Why Pass Additional Data? + +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. + +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. + +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 + +### API Routes List + +- 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) + +*** + +## 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: + +```ts title="src/api/middlewares.ts" +import { defineMiddlewares } from "@medusajs/framework/http" +import { z } from "zod" + +export default defineMiddlewares({ + routes: [ + { + method: "POST", + matcher: "/admin/products", + additionalDataValidator: { + brand: z.string().optional(), + }, + }, + ], +}) +``` + +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. + +Refer to [Zod's documentation](https://zod.dev) for all available validation rules. + +### 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. + +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"] + } + ], + "shipping_profile_id": "{shipping_profile_id}", + "additional_data": { + "brand": "Acme" + } +}' +``` + +Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID. + +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. + +*** + +## Use Additional Data in a Hook + +Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). + +Step functions consuming the workflow hook can access the `additional_data` in the first parameter. + +For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. + +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" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + if (!additional_data?.brand) { + return + } + + const productModuleService = container.resolve( + Modules.PRODUCT + ) + + await productModuleService.upsertProducts( + products.map((product) => ({ + ...product, + metadata: { + ...product.metadata, + brand: additional_data.brand, + }, + })) + ) + + return new StepResponse(products, { + products, + additional_data, + }) + } +) +``` + +This consumes the `productsCreated` hook, which runs after the products are created. + +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. + +### Compensation Function + +Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. + +For example, pass the following second parameter to the `productsCreated` hook: + +```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 + ) + } +) +``` + +This updates the products to their original state before adding the brand to their `metadata` property. + + # Seed Data with Custom CLI Script In this chapter, you'll learn how to seed data using a custom CLI script. @@ -8201,312 +8400,6 @@ export default defineMiddlewares({ This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. -# 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. - -## Why Pass Additional Data? - -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. - -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. - -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 - -### API Routes List - -- 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) - -*** - -## 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: - -```ts title="src/api/middlewares.ts" -import { defineMiddlewares } from "@medusajs/framework/http" -import { z } from "zod" - -export default defineMiddlewares({ - routes: [ - { - method: "POST", - matcher: "/admin/products", - additionalDataValidator: { - brand: z.string().optional(), - }, - }, - ], -}) -``` - -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. - -Refer to [Zod's documentation](https://zod.dev) for all available validation rules. - -### 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. - -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"] - } - ], - "shipping_profile_id": "{shipping_profile_id}", - "additional_data": { - "brand": "Acme" - } -}' -``` - -Make sure to replace the `{token}` in the authorization header with an admin user's authentication token, and `{shipping_profile_id}` with an existing shipping profile's ID. - -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. - -*** - -## Use Additional Data in a Hook - -Learn about workflow hooks in [this guide](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md). - -Step functions consuming the workflow hook can access the `additional_data` in the first parameter. - -For example, consider you want to store the data passed in `additional_data` in the product's `metadata` property. - -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" - -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - if (!additional_data?.brand) { - return - } - - const productModuleService = container.resolve( - Modules.PRODUCT - ) - - await productModuleService.upsertProducts( - products.map((product) => ({ - ...product, - metadata: { - ...product.metadata, - brand: additional_data.brand, - }, - })) - ) - - return new StepResponse(products, { - products, - additional_data, - }) - } -) -``` - -This consumes the `productsCreated` hook, which runs after the products are created. - -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. - -### Compensation Function - -Hooks also accept a compensation function as a second parameter to undo the actions made by the step function. - -For example, pass the following second parameter to the `productsCreated` hook: - -```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 - ) - } -) -``` - -This updates the products to their original state before adding the brand to their `metadata` property. - - -# 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. - -## Throw MedusaError - -When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. - -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 -) => { - if (!req.query.q) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "The `q` query parameter is required." - ) - } - - // ... -} -``` - -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. - -### Error Object in Response - -The error object returned in the response has two properties: - -- `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. - -### MedusaError Types - -|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\`| - -*** - -## Override Error Handler - -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. - -This error handler will also be used for errors thrown in Medusa's API routes and resources. - -For example, create `src/api/middlewares.ts` with the following: - -```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.", - }) - }, -}) -``` - -The `errorHandler` property's value is a function that accepts four parameters: - -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. - -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. - - # HTTP Methods In this chapter, you'll learn about how to add new API routes for each HTTP method. @@ -8930,6 +8823,113 @@ If you need to change the middlewares applied to a route, you can create a custo Learn more in the [Override API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/override/index.html.md) chapter. +# 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. + +## Throw MedusaError + +When throwing an error in your API routes, middlewares, workflows, or any customization, throw a `MedusaError` from the Medusa Framework. + +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 +) => { + if (!req.query.q) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "The `q` query parameter is required." + ) + } + + // ... +} +``` + +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. + +### Error Object in Response + +The error object returned in the response has two properties: + +- `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. + +### MedusaError Types + +|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\`| + +*** + +## Override Error Handler + +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. + +This error handler will also be used for errors thrown in Medusa's API routes and resources. + +For example, create `src/api/middlewares.ts` with the following: + +```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.", + }) + }, +}) +``` + +The `errorHandler` property's value is a function that accepts four parameters: + +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. + +This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. + + # Override API Routes In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa. @@ -9126,177 +9126,6 @@ You can apply validation rules on received body parameters to ensure they match Learn more in [this documentation](https://docs.medusajs.com/learn/fundamentals/api-routes/validation#how-to-validate-request-body/index.html.md). -# Configure Request Body Parser - -In this chapter, you'll learn how to configure the request body parser for your API routes. - -## Default Body Parser Configuration - -The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests. - -This chapter shares some examples of configuring the body parser for different data types or use cases. - -*** - -## Preserve Raw Body Data for Webhooks - -If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property. - -To do that, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={preserveHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { preserveRawBody: true }, - matcher: "/custom", - }, - ], -}) -``` - -The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property. - -Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). - -You can then access the raw body data in your API route handler: - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - console.log(req.rawBody) - - // TODO use raw body -} -``` - -*** - -## Configure Request Body Size Limit - -By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error. - -You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example: - -```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - bodyParser: { sizeLimit: "2mb" }, - matcher: "/custom", - }, - ], -}) -``` - -The `sizeLimit` property accepts one of the following types of values: - -- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size. -- A number representing the size limit in bytes. For example, `1024` for 1kb. - -*** - -## Configure File Uploads - -To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route. - -The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency: - -```bash npm2yarn -npm install --save-dev @types/multer -``` - -Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={uploadHighlights} -import { defineMiddlewares } from "@medusajs/framework/http" -import multer from "multer" - -const upload = multer({ storage: multer.memoryStorage() }) - -export default defineMiddlewares({ - routes: [ - { - method: ["POST"], - matcher: "/custom", - middlewares: [ - // @ts-ignore - upload.array("files"), - ], - }, - ], -}) -``` - -In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name. - -You can then access the uploaded files in your API route handler: - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] - - // TODO handle files -} -``` - -The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`. - -### Uploading Files using File Module Provider - -The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md): - -```ts title="src/api/custom/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const files = req.files as Express.Multer.File[] - - if (!files?.length) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "No files were uploaded" - ) - } - - const { result } = await uploadFilesWorkflow(req.scope).run({ - input: { - files: files?.map((f) => ({ - filename: f.originalname, - mimeType: f.mimetype, - content: f.buffer.toString("binary"), - access: "public", - })), - }, - }) - - res.status(200).json({ files: result }) -} -``` - -Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. - - # Protected API Routes In this chapter, you’ll learn how to create protected API routes. @@ -9562,6 +9391,259 @@ Then, you use Query to retrieve the user details. The `throwIfKeyNotFound` optio After that, you can use the user's details in your API route. +# Retrieve Custom Links from Medusa's API Route + +In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. + +## Why Retrieve Custom Linked Data Models? + +Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. + +For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model. + +When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. + +*** + +## How to Retrieve Custom Linked Data Models Using `fields`? + +Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. + +For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid): + +```bash +curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ +-H 'Authorization: Bearer {access_token}' +``` + +The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations). + +By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. + +*** + +## API Routes that Restrict Retrievable Fields + +Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. + +The API routes that restrict the fields and relations you can retrieve are: + +- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers) +- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers) +- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories) + +### How to Override Allowed Fields and Relations + +For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes. + +For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: + +Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. + +```ts title="src/api/middlewares.ts" highlights={highlights} +import { defineMiddlewares } from "@medusajs/medusa" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: "GET", + middlewares: [ + (req, res, next) => { + req.allowed?.push("b2b_company") + next() + }, + ], + }, + ], +}) +``` + +In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid). + +The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. + +You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid): + +```bash +curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ +-H 'Authorization: Bearer {access_token}' +``` + +In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. + + +# Configure Request Body Parser + +In this chapter, you'll learn how to configure the request body parser for your API routes. + +## Default Body Parser Configuration + +The Medusa application configures the body parser by default to parse JSON, URL-encoded, and text request content types. You can parse other data types by adding the relevant [Express middleware](https://expressjs.com/en/guide/using-middleware.html) or preserve the raw body data by configuring the body parser, which is useful for webhook requests. + +This chapter shares some examples of configuring the body parser for different data types or use cases. + +*** + +## Preserve Raw Body Data for Webhooks + +If your API route receives webhook requests, you might want to preserve the raw body data. To do this, you can configure the body parser to parse the raw body data and store it in the `req.rawBody` property. + +To do that, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" highlights={preserveHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { preserveRawBody: true }, + matcher: "/custom", + }, + ], +}) +``` + +The middleware route object passed to `routes` accepts a `bodyParser` property whose value is an object of configuration for the default body parser. By enabling the `preserveRawBody` property, the raw body data is preserved and stored in the `req.rawBody` property. + +Learn more about [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). + +You can then access the raw body data in your API route handler: + +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + console.log(req.rawBody) + + // TODO use raw body +} +``` + +*** + +## Configure Request Body Size Limit + +By default, the body parser limits the request body size to `100kb`. If a request body exceeds that size, the Medusa application throws an error. + +You can configure the body parser to accept larger request bodies by setting the `sizeLimit` property of the `bodyParser` object in a middleware route object. For example: + +```ts title="src/api/middlewares.ts" highlights={sizeLimitHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + bodyParser: { sizeLimit: "2mb" }, + matcher: "/custom", + }, + ], +}) +``` + +The `sizeLimit` property accepts one of the following types of values: + +- A string representing the size limit in bytes (For example, `100kb`, `2mb`, `5gb`). It is passed to the [bytes](https://www.npmjs.com/package/bytes) library to parse the size. +- A number representing the size limit in bytes. For example, `1024` for 1kb. + +*** + +## Configure File Uploads + +To accept file uploads in your API routes, you can configure the [Express Multer middleware](https://expressjs.com/en/resources/middleware/multer.html) on your route. + +The `multer` package is available through the `@medusajs/medusa` package, so you don't need to install it. However, for better typing support, install the `@types/multer` package as a development dependency: + +```bash npm2yarn +npm install --save-dev @types/multer +``` + +Then, to configure file upload for your route, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" highlights={uploadHighlights} +import { defineMiddlewares } from "@medusajs/framework/http" +import multer from "multer" + +const upload = multer({ storage: multer.memoryStorage() }) + +export default defineMiddlewares({ + routes: [ + { + method: ["POST"], + matcher: "/custom", + middlewares: [ + // @ts-ignore + upload.array("files"), + ], + }, + ], +}) +``` + +In the example above, you configure the `multer` middleware to store the uploaded files in memory. Then, you apply the `upload.array("files")` middleware to the route to accept file uploads. By using the `array` method, you accept multiple file uploads with the same `files` field name. + +You can then access the uploaded files in your API route handler: + +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const files = req.files as Express.Multer.File[] + + // TODO handle files +} +``` + +The uploaded files are stored in the `req.files` property as an array of Multer file objects that have properties like `filename` and `mimetype`. + +### Uploading Files using File Module Provider + +The recommended way to upload the files to storage using the configured [File Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md) is to use the [uploadFilesWorkflow](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md): + +```ts title="src/api/custom/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import { uploadFilesWorkflow } from "@medusajs/medusa/core-flows" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const files = req.files as Express.Multer.File[] + + if (!files?.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "No files were uploaded" + ) + } + + const { result } = await uploadFilesWorkflow(req.scope).run({ + input: { + files: files?.map((f) => ({ + filename: f.originalname, + mimeType: f.mimetype, + content: f.buffer.toString("binary"), + access: "public", + })), + }, + }) + + res.status(200).json({ files: result }) +} +``` + +Check out the [uploadFilesWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/uploadFilesWorkflow/index.html.md) for details on the expected input and output of the workflow. + + # API Route Response In this chapter, you'll learn how to send a response in your API route. @@ -9664,88 +9746,6 @@ This API route opens a stream by setting the `Content-Type` in the header to `te 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. -# Retrieve Custom Links from Medusa's API Route - -In this chapter, you'll learn how to retrieve custom data models linked to existing Medusa data models from Medusa's API routes. - -## Why Retrieve Custom Linked Data Models? - -Often, you'll link custom data models to existing Medusa data models to implement custom features or expand on existing ones. - -For example, to add brands for products, you can create a `Brand` data model in a Brand Module, then [define a link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md)'s `Product` data model. - -When you implement this customization, you might need to retrieve the brand of a product using the existing [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid). You can do this by passing the linked data model's name in the `fields` query parameter of the API route. - -*** - -## How to Retrieve Custom Linked Data Models Using `fields`? - -Most of Medusa's API routes accept a `fields` query parameter that allows you to specify the fields and relations to retrieve in the resource, such as a product. - -For example, to retrieve the brand of a product, you can pass the `brand` field in the `fields` query parameter of the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid): - -```bash -curl 'http://localhost:9000/admin/products/{id}?fields=*brand' \ --H 'Authorization: Bearer {access_token}' -``` - -The `fields` query parameter accepts a comma-separated list of fields and relations to retrieve. To learn more about using the `fields` query parameter, refer to the [API Reference](https://docs.medusajs.com/api/store#select-fields-and-relations). - -By prefixing `brand` with an asterisk (`*`), you retrieve all the default fields of the product, including the `brand` field. If you don't include the `*` prefix, the response will only include the product's brand. - -*** - -## API Routes that Restrict Retrievable Fields - -Some of Medusa's API routes restrict the fields and relations you can retrieve, which means you can't pass your custom linked data models in the `fields` query parameter. Medusa makes this restriction to ensure the API routes are performant and secure. - -The API routes that restrict the fields and relations you can retrieve are: - -- [Customer Store API Routes](https://docs.medusajs.com/api/store#customers) -- [Customer Admin API Routes](https://docs.medusajs.com/api/admin#customers) -- [Product Category Admin API Routes](https://docs.medusajs.com/api/admin#product-categories) - -### How to Override Allowed Fields and Relations - -For these routes, you need to override the allowed fields and relations to be retrieved. You can do this by adding a [middleware](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) to those routes. - -For example, to allow retrieving the `b2b_company` of a customer using the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid), create the file `src/api/middlewares.ts` with the following content: - -Learn how to create a middleware in the [Middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md) chapter. - -```ts title="src/api/middlewares.ts" highlights={highlights} -import { defineMiddlewares } from "@medusajs/medusa" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/store/customers/me", - method: "GET", - middlewares: [ - (req, res, next) => { - req.allowed?.push("b2b_company") - next() - }, - ], - }, - ], -}) -``` - -In this example, you apply a middleware to the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid). - -The request object passed to middlewares has an `allowed` property that contains the fields and relations that can be retrieved. So, you modify the `allowed` array to include the `b2b_company` field. - -You can now retrieve the `b2b_company` field using the `fields` query parameter of the [Get Customer Admin API Route](https://docs.medusajs.com/api/admin#customers_getcustomersid): - -```bash -curl 'http://localhost:9000/admin/customers/{id}?fields=*b2b_company' \ --H 'Authorization: Bearer {access_token}' -``` - -In this example, you retrieve the `b2b_company` relation of the customer using the `fields` query parameter. - - # Request Body and Query Parameter Validation In this chapter, you'll learn how to validate request body and query parameters in your custom API route. @@ -10179,6 +10179,226 @@ export default MyCustom This creates a unique composite index on the `name` and `age` properties. +# Manage Relationships + +In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. + +This chapter applies to data model relationships within the same module. To manage linked data models across modules, check out [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +## Manage One-to-One Relationship + +### BelongsTo Side of One-to-One + +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}_id` property, where `{relation}` is the name of the relation property. + +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: + +```ts highlights={belongsHighlights} +// when creating an email +const email = await helloModuleService.createEmails({ + // other properties... + user_id: "123", +}) + +// when updating an email +const email = await helloModuleService.updateEmails({ + id: "321", + // other properties... + user_id: "123", +}) +``` + +In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. + +### HasOne Side + +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. + +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", +}) + +// when updating a user +const user = await helloModuleService.updateUsers({ + id: "321", + // other properties... + email: "123", +}) +``` + +In the example above, you pass the `email` property when creating or updating a user to specify the email it has. + +*** + +## Manage One-to-Many Relationship + +In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. + +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. + +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: + +```ts highlights={manyBelongsHighlights} +// when creating a product +const product = await helloModuleService.createProducts({ + // other properties... + store_id: "123", +}) + +// 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, 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 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 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"], +}) +``` + +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. + +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={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", + ], +}) +``` + +This keeps existing associations between the product and orders, and adds a new one. + +*** + +## Manage Many-to-Many Relationship with pivotEntity + +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. + +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: + +```ts highlights={["4"]} +class BlogModuleService extends MedusaService({ + Order, + Product, + OrderProduct, +}) {} +``` + +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. + +For example: + +```ts +// create order-product association +const orderProduct = await blogModuleService.createOrderProducts({ + order_id: "123", + product_id: "123", + metadata: { + test: true, + }, +}) + +// update order-product association +const orderProduct = await blogModuleService.updateOrderProducts({ + id: "123", + metadata: { + test: false, + }, +}) + +// delete order-product association +await blogModuleService.deleteOrderProducts("123") +``` + +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`. + +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. + +*** + +## 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. + +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: + +```ts highlights={retrieveHighlights} +const product = await blogModuleService.retrieveProducts( + "123", + { + relations: ["orders"], + } +) +``` + +In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. + + # Infer Type of Data Model In this chapter, you'll learn how to infer the type of a data model. @@ -10571,226 +10791,6 @@ const posts = await blogModuleService.listPosts({ This retrieves records that include `New Products` in their `title` property. -# Manage Relationships - -In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. - -This chapter applies to data model relationships within the same module. To manage linked data models across modules, check out [Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -## Manage One-to-One Relationship - -### BelongsTo Side of One-to-One - -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}_id` property, where `{relation}` is the name of the relation property. - -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: - -```ts highlights={belongsHighlights} -// when creating an email -const email = await helloModuleService.createEmails({ - // other properties... - user_id: "123", -}) - -// when updating an email -const email = await helloModuleService.updateEmails({ - id: "321", - // other properties... - user_id: "123", -}) -``` - -In the example above, you pass the `user_id` property when creating or updating an email to specify the user it belongs to. - -### HasOne Side - -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. - -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", -}) - -// when updating a user -const user = await helloModuleService.updateUsers({ - id: "321", - // other properties... - email: "123", -}) -``` - -In the example above, you pass the `email` property when creating or updating a user to specify the email it has. - -*** - -## Manage One-to-Many Relationship - -In a one-to-many relationship, you can only manage the associations from the `belongsTo` side. - -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. - -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: - -```ts highlights={manyBelongsHighlights} -// when creating a product -const product = await helloModuleService.createProducts({ - // other properties... - store_id: "123", -}) - -// 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, 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 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 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"], -}) -``` - -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. - -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={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", - ], -}) -``` - -This keeps existing associations between the product and orders, and adds a new one. - -*** - -## Manage Many-to-Many Relationship with pivotEntity - -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. - -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: - -```ts highlights={["4"]} -class BlogModuleService extends MedusaService({ - Order, - Product, - OrderProduct, -}) {} -``` - -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. - -For example: - -```ts -// create order-product association -const orderProduct = await blogModuleService.createOrderProducts({ - order_id: "123", - product_id: "123", - metadata: { - test: true, - }, -}) - -// update order-product association -const orderProduct = await blogModuleService.updateOrderProducts({ - id: "123", - metadata: { - test: false, - }, -}) - -// delete order-product association -await blogModuleService.deleteOrderProducts("123") -``` - -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`. - -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. - -*** - -## 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. - -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: - -```ts highlights={retrieveHighlights} -const product = await blogModuleService.retrieveProducts( - "123", - { - relations: ["orders"], - } -) -``` - -In the example above, the retrieved product has an `orders` property, whose value is an array of orders associated with the product. - - # Data Model Relationships In this chapter, you’ll learn how to define relationships between data models in your module. @@ -11086,6 +11086,149 @@ The `cascades` method accepts an object. Its key is the operation’s name, such In the example above, when a store is deleted, its associated products are also deleted. +# Event Data Payload + +In this chapter, you'll learn how subscribers receive an event's data payload. + +## Access Event's Data Payload + +When events are emitted, they’re emitted with a data payload. + +The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. + +For example: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" + +export default async function productCreateHandler({ + event, +}: SubscriberArgs<{ id: string }>) { + const productId = event.data.id + console.log(`The product ${productId} was created`) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `event` object has the following properties: + +- data: (\`object\`) The data payload of the event. Its properties are different for each event. +- name: (string) The name of the triggered event. +- metadata: (\`object\`) Additional data and context of the emitted event. + +This logs the product ID received in the `product.created` event’s data payload to the console. + +{/* --- + +## List of Events with Data Payload + +Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} + + +# Migrations + +In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. + +## What is a Migration? + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +The migration's file has a class with two methods: + +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. + +*** + +## Generate Migration + +Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. + +For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: + +```bash +npx medusa db:generate blog +``` + +This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). + +*** + +## Write a Migration Manually + +You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. + +For example: + +```ts title="src/modules/blog/migrations/Migration202507021059.ts" +import { Migration } from "@mikro-orm/migrations" + +export class Migration202507021059 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } + +} +``` + +The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. + +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. + +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. + +*** + +## Run the Migration + +To run your migration, run the following command: + +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate +``` + +This reflects the changes in the database as implemented in the migration's `up` method. + +*** + +## Rollback the Migration + +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback blog +``` + +This rolls back the last ran migration on the Blog Module. + +### Caution: Rollback Migration before Deleting + +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. + +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. + +*** + +## More Database Commands + +To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). + + # Emit Workflow and Service Events In this chapter, you'll learn about event types and how to emit an event in a service or workflow. @@ -11254,583 +11397,6 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Migrations - -In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. - -## What is a Migration? - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -The migration's file has a class with two methods: - -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. - -*** - -## Generate Migration - -Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. - -For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: - -```bash -npx medusa db:generate blog -``` - -This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). - -*** - -## Write a Migration Manually - -You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. - -For example: - -```ts title="src/modules/blog/migrations/Migration202507021059.ts" -import { Migration } from "@mikro-orm/migrations" - -export class Migration202507021059 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } - -} -``` - -The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. - -In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. - -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. - -*** - -## Run the Migration - -To run your migration, run the following command: - -This command also syncs module links. If you don't want that, use the `--skip-links` option. - -```bash -npx medusa db:migrate -``` - -This reflects the changes in the database as implemented in the migration's `up` method. - -*** - -## Rollback the Migration - -To rollback or revert the last migration you ran for a module, run the following command: - -```bash -npx medusa db:rollback blog -``` - -This rolls back the last ran migration on the Blog Module. - -### Caution: Rollback Migration before Deleting - -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. - -So, always rollback the migration before deleting it. - -*** - -## More Database Commands - -To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). - - -# Event Data Payload - -In this chapter, you'll learn how subscribers receive an event's data payload. - -## Access Event's Data Payload - -When events are emitted, they’re emitted with a data payload. - -The object that the subscriber function receives as a parameter has an `event` property, which is an object holding the event payload in a `data` property with additional context. - -For example: - -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-5" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" - -export default async function productCreateHandler({ - event, -}: SubscriberArgs<{ id: string }>) { - const productId = event.data.id - console.log(`The product ${productId} was created`) -} - -export const config: SubscriberConfig = { - event: "product.created", -} -``` - -The `event` object has the following properties: - -- data: (\`object\`) The data payload of the event. Its properties are different for each event. -- name: (string) The name of the triggered event. -- metadata: (\`object\`) Additional data and context of the emitted event. - -This logs the product ID received in the `product.created` event’s data payload to the console. - -{/* --- - -## List of Events with Data Payload - -Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} - - -# Create a Plugin - -In this chapter, you'll learn how to create a Medusa plugin and publish it. - -A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. - -Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## 1. Create a Plugin Project - -Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. - -Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: - -```bash -npx create-medusa-app my-plugin --plugin -``` - -This will create a new Medusa plugin project in the `my-plugin` directory. - -### Plugin Directory Structure - -After the installation is done, the plugin structure will look like this: - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -- `src/`: Contains the Medusa customizations. -- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). -- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. -- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). -- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). -- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). -- `src/provider`: Contains [module providers](#create-module-providers). -- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). -- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. -- `package.json`: Contains the plugin's package information, including general information and dependencies. -- `tsconfig.json`: Contains the TypeScript configuration for the plugin. - -*** - -## 2. Prepare Plugin - -### Package Name - -Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. - -For example: - -```json title="package.json" -{ - "name": "@myorg/plugin-name", - // ... -} -``` - -### Package Keywords - -Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. - -Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. - -```json title="package.json" -{ - "keywords": [ - "medusa-plugin-integration", - "medusa-v2" - ], - // ... -} -``` - -In addition, make sure to use one of the following keywords based on your integration type: - -|Keyword|Description|Example| -|---|---|---| -|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| -|\`medusa-plugin-auth\`|Authentication service integration|Auth0| -|\`medusa-plugin-cms\`|CMS service integration|Contentful| -|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| -|\`medusa-plugin-payment\`|Payment service integration|PayPal| -|\`medusa-plugin-search\`|Search service integration|MeiliSearch| -|\`medusa-plugin-shipping\`|Shipping service integration|DHL| -|\`medusa-plugin-other\`|Other third-party integrations|Sentry| - -### Package Dependencies - -Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. - -In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. - -For example, assuming `2.5.0` is the latest Medusa version: - -```json title="package.json" -{ - "devDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/ui": "4.0.4", - "@medusajs/icons": "2.5.0", - "@swc/core": "1.5.7", - }, - "peerDependencies": { - "@medusajs/admin-sdk": "2.5.0", - "@medusajs/cli": "2.5.0", - "@medusajs/framework": "2.5.0", - "@medusajs/test-utils": "2.5.0", - "@medusajs/medusa": "2.5.0", - "@medusajs/ui": "4.0.3", - "@medusajs/icons": "2.5.0", - } -} -``` - -### Package Exports - -Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. - -In the `package.json` file, make sure your plugin has the following exports: - -```json title="package.json" -{ - "exports": { - "./package.json": "./package.json", - "./workflows": "./.medusa/server/src/workflows/index.js", - "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", - "./providers/*": "./.medusa/server/src/providers/*/index.js", - "./admin": { - "import": "./.medusa/server/src/admin/index.mjs", - "require": "./.medusa/server/src/admin/index.js", - "default": "./.medusa/server/src/admin/index.js" - }, - "./*": "./.medusa/server/src/*.js" - } -} -``` - -Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. - -The plugin exports the following files and directories: - -- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. -- `./workflows`: The workflows exported in `./src/workflows/index.ts`. -- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. -- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. -- `./admin`: The admin extensions exported in `./src/admin/index.ts`. -- `./*`: Any other files in the plugin's `src` directory. - -*** - -## 3. Publish Plugin Locally for Development and Testing - -Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. - -### Publish and Install Local Package - -### Prerequisites - -- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) - -The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. - -To publish the plugin to the local registry, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, navigate to your Medusa application: - -```bash title="Medusa application" -cd ~/path/to/medusa-app -``` - -Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. - -Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm install --save-dev yalc -``` - -After that, run the following Medusa CLI command to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add @myorg/plugin-name -``` - -Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. - -### Register Plugin in Medusa Application - -After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. - -Add the plugin to the `plugins` array in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" highlights={pluginHighlights} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: {}, - }, - ], -}) -``` - -The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. - -#### Pass Module Options through Plugin - -Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. - -For example: - -```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "@myorg/plugin-name", - options: { - apiKey: true, - }, - }, - ], -}) -``` - -The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). - -### Watch Plugin Changes During Development - -While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. - -To do that, run the following command in your plugin project: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -This command will: - -- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. -- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. - -### Start Medusa Application - -You can start your Medusa application's development server to test out your plugin: - -```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" -npm run dev -``` - -While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. - -*** - -## 4. Create Customizations in the Plugin - -You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. - -- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) -- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) -- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) -- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) -- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) -- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) -- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) -- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) -- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) - -While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). - -### Generating Migrations for Modules - -During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: - -```plain title="Plugin project" -DB_USERNAME=postgres -DB_PASSWORD=123... -DB_HOST=localhost -DB_PORT=5432 -DB_NAME=db_name -``` - -You can add these environment variables in a `.env` file in your plugin project. The variables are: - -- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. -- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. -- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. -- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. -- `DB_NAME`: The name of the PostgreSQL database to connect to. - -Then, run the following command in your plugin project to generate migrations for the modules in your plugin: - -```bash title="Plugin project" -npx medusa plugin:db:generate -``` - -This command generates migrations for all modules in the plugin. - -Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: - -```bash title="Medusa application" -npx medusa db:migrate -``` - -The migrations in your application, including your plugin, will run and update the database. - -### Importing Module Resources - -In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. - -These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. - -For example, to import the plugin's workflow in your Medusa application: - -`@myorg/plugin-name` is the plugin package's name. - -```ts -import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" -import BlogModule from "@myorg/plugin-name/modules/blog" -// import other files created in plugin like ./src/types/blog.ts -import BlogType from "@myorg/plugin-name/types/blog" -``` - -### Create Module Providers - -The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. - -For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: - -`@myorg/plugin-name` is the plugin package's name. - -```ts highlights={[["9"]]} title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@myorg/plugin-name/providers/my-notification", - id: "my-notification", - options: { - channels: ["email"], - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. - -To learn how to create module providers, refer to the following guides: - -- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) -- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) -- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) -- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) -- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) -- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) - -*** - -## 5. Publish Plugin to NPM - -Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. - -Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: - -```bash -npx medusa plugin:build -``` - -The command will compile an output in the `.medusa/server` directory. - -You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: - -```bash -npm publish -``` - -If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. - -### Install Public Plugin in Medusa Application - -You install a plugin that's published publicly using your package manager. For example: - -```bash npm2yarn -npm install @myorg/plugin-name -``` - -Where `@myorg/plugin-name` is the name of your plugin as published on NPM. - -Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). - -*** - -## Update a Published Plugin - -To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). - -If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. - -First, run the following command to change the version of the plugin: - -```bash -npm version -``` - -Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. - -Then, re-run the same commands for publishing a plugin: - -```bash -npx medusa plugin:build -npm publish -``` - -This will publish an updated version of your plugin under a new version. - - # Add Columns to a Link Table In this chapter, you'll learn how to add custom columns to a link definition's table and manage them. @@ -12810,232 +12376,6 @@ Try passing one of the Query configuration parameters, like `fields` or `limit`, Learn more about [specifing fields and relations](https://docs.medusajs.com/api/store#select-fields-and-relations) and [pagination](https://docs.medusajs.com/api/store#pagination) in the API reference. -# Query Context - -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). - -## What is Query Context? - -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). - -*** - -## How to Use Query Context - -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`). - -You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. - -For example, to retrieve posts using Query while passing the user's language as a context: - -```ts -const { data } = await query.graph({ - entity: "post", - fields: ["*"], - context: QueryContext({ - lang: "es", - }), -}) -``` - -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. - -For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: - -```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 -``` - -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. - -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". - -Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). - -### Using Pagination with Query - -If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. - -For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: - -```ts -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 listAndCountPosts( - filters?: any, - config?: FindConfig | undefined, - @MedusaContext() sharedContext?: Context | undefined - ) { - const context = filters.context ?? {} - delete filters.context - - const result = await super.listAndCountPosts( - filters, - config, - sharedContext - ) - - if (context.lang === "es") { - result.posts = posts.map((post) => { - return { - ...post, - title: post.title + " en español", - } - }) - } - - return result - } -} - -export default BlogModuleService -``` - -Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. - -*** - -## Passing Query Context to Related Data Models - -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 linked data models, check out the [next section](#passing-query-context-to-linked-data-models). - -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", - }), - }), -}) -``` - -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" - -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) - - 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 BlogModuleService -``` - -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 - -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. - -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). - - # Read-Only Module Link In this chapter, you’ll learn what a read-only module link is and how to define one. @@ -13542,78 +12882,230 @@ If multiple posts have their `product_id` set to a product's ID, an array of pos [Sanity Integration Tutorial](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md). -# Scheduled Jobs Number of Executions +# Query Context -In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. +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). -## numberOfExecutions Option +## What is Query Context? -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. +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: - -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") -} - -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, -} -``` - -The above scheduled job has the `numberOfExecutions` configuration set to `3`. - -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. - -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. - - -# Commerce Modules - -In this chapter, you'll learn about Medusa's Commerce Modules. - -## What is a Commerce Module? - -Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. - -Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. - -You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) - -The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. - -### List of Medusa's Commerce Modules - -Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa. +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). *** -## Use Commerce Modules in Custom Flows +## How to Use Query Context -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. +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`). -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: +You initialize a context using `QueryContext` from the Modules SDK. It accepts an object of contexts as an argument. -```ts highlights={highlights} -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +For example, to retrieve posts using Query while passing the user's language as a context: -export const countProductsStep = createStep( - "count-products", - async ({ }, { container }) => { - const productModuleService = container.resolve("product") - - const [,count] = await productModuleService.listAndCountProducts() - - return new StepResponse(count) - } -) +```ts +const { data } = await query.graph({ + entity: "post", + fields: ["*"], + context: QueryContext({ + lang: "es", + }), +}) ``` -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. +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. + +For example, continuing the example above, you can override the `listPosts` method of the Blog Module's service to handle the context: + +```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 +``` + +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. + +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". + +Learn more about the generated `list` method in [this reference](https://docs.medusajs.com/resources/service-factory-reference/methods/list/index.html.md). + +### Using Pagination with Query + +If you pass pagination fields to `query.graph`, you must also override the `listAndCount` method in the service. + +For example, following along with the previous example, you must override the `listAndCountPosts` method of the Blog Module's service: + +```ts +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 listAndCountPosts( + filters?: any, + config?: FindConfig | undefined, + @MedusaContext() sharedContext?: Context | undefined + ) { + const context = filters.context ?? {} + delete filters.context + + const result = await super.listAndCountPosts( + filters, + config, + sharedContext + ) + + if (context.lang === "es") { + result.posts = posts.map((post) => { + return { + ...post, + title: post.title + " en español", + } + }) + } + + return result + } +} + +export default BlogModuleService +``` + +Now, the `listAndCountPosts` method will handle the context passed to `query.graph` when you pass pagination fields. You can also move the logic to transform the posts' titles to a separate method and call it from both `listPosts` and `listAndCountPosts`. + +*** + +## Passing Query Context to Related Data Models + +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 linked data models, check out the [next section](#passing-query-context-to-linked-data-models). + +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", + }), + }), +}) +``` + +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" + +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) + + 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 BlogModuleService +``` + +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 + +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. + +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). # Module Container @@ -13682,6 +13174,50 @@ export default async function helloWorldLoader({ ``` +# Commerce Modules + +In this chapter, you'll learn about Medusa's Commerce Modules. + +## What is a Commerce Module? + +Commerce Modules are built-in [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. + +Medusa's Commerce Modules are used to form Medusa's default [workflows](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) and [APIs](https://docs.medusajs.com/api/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. + +You'll find the details and steps of the add-to-cart workflow in [this workflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/addToCartWorkflow/index.html.md) + +The core commerce logic contained in Commerce Modules is also available directly when you are building customizations. This granular access to commerce functionality is unique and expands what's possible to build with Medusa drastically. + +### List of Medusa's Commerce Modules + +Refer to [this reference](https://docs.medusajs.com/resources/commerce-modules/index.html.md) for a full list of Commerce Modules in Medusa. + +*** + +## Use Commerce Modules in Custom Flows + +Similar to your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), the Medusa application registers a Commerce Module's service in the [container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. + +For example, consider you have a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: + +```ts highlights={highlights} +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const countProductsStep = createStep( + "count-products", + async ({ }, { container }) => { + const productModuleService = container.resolve("product") + + const [,count] = await productModuleService.listAndCountProducts() + + return new StepResponse(count) + } +) +``` + +Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features. + + # Perform Database Operations in a Service In this chapter, you'll learn how to perform database operations in a module's service. @@ -14321,6 +13857,33 @@ There are different Infrastructure Module types including: Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. +# Modules Directory Structure + +In this document, you'll learn about the expected files and directories in your module. + +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) + +## index.ts + +The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## service.ts + +A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +*** + +## Other Directories + +The following directories are optional and their content are explained more in the following chapters: + +- `models`: Holds the data models representing tables in the database. +- `migrations`: Holds the migration files used to reflect changes on the database. +- `loaders`: Holds the scripts to run on the Medusa application's start-up. + + # Module Isolation In this chapter, you'll learn how modules are isolated, and what that means for your custom development. @@ -14675,33 +14238,6 @@ info: Connected to MongoDB You can now resolve the MongoDB Module's main service in your customizations to perform operations on the MongoDB database. -# Modules Directory Structure - -In this document, you'll learn about the expected files and directories in your module. - -![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) - -## index.ts - -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## service.ts - -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). - -*** - -## Other Directories - -The following directories are optional and their content are explained more in the following chapters: - -- `models`: Holds the data models representing tables in the database. -- `migrations`: Holds the migration files used to reflect changes on the database. -- `loaders`: Holds the scripts to run on the Medusa application's start-up. - - # Multiple Services in a Module In this chapter, you'll learn how to use multiple services in a module. @@ -15208,6 +14744,470 @@ export default BlogModuleService ``` +# Create a Plugin + +In this chapter, you'll learn how to create a Medusa plugin and publish it. + +A [plugin](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) is a package of reusable Medusa customizations that you can install in any Medusa application. By creating and publishing a plugin, you can reuse your Medusa customizations across multiple projects or share them with the community. + +Plugins are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## 1. Create a Plugin Project + +Plugins are created in a separate Medusa project. This makes the development and publishing of the plugin easier. Later, you'll install that plugin in your Medusa application to test it out and use it. + +Medusa's `create-medusa-app` CLI tool provides the option to create a plugin project. Run the following command to create a new plugin project: + +```bash +npx create-medusa-app my-plugin --plugin +``` + +This will create a new Medusa plugin project in the `my-plugin` directory. + +### Plugin Directory Structure + +After the installation is done, the plugin structure will look like this: + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +- `src/`: Contains the Medusa customizations. +- `src/admin`: Contains [admin extensions](https://docs.medusajs.com/learn/fundamentals/admin/index.html.md). +- `src/api`: Contains [API routes](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) and [middlewares](https://docs.medusajs.com/learn/fundamentals/api-routes/middlewares/index.html.md). You can add store, admin, or any custom API routes. +- `src/jobs`: Contains [scheduled jobs](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). +- `src/links`: Contains [module links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). +- `src/modules`: Contains [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +- `src/provider`: Contains [module providers](#create-module-providers). +- `src/subscribers`: Contains [subscribers](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). +- `src/workflows`: Contains [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). You can also add [hooks](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) under `src/workflows/hooks`. +- `package.json`: Contains the plugin's package information, including general information and dependencies. +- `tsconfig.json`: Contains the TypeScript configuration for the plugin. + +*** + +## 2. Prepare Plugin + +### Package Name + +Before developing, testing, and publishing your plugin, make sure its name in `package.json` is correct. This is the name you'll use to install the plugin in your Medusa application. + +For example: + +```json title="package.json" +{ + "name": "@myorg/plugin-name", + // ... +} +``` + +### Package Keywords + +Medusa scrapes NPM for a list of plugins that integrate third-party services, to later showcase them on the Medusa website. If you want your plugin to appear in that listing, make sure to add the `medusa-v2` and `medusa-plugin-integration` keywords to the `keywords` field in `package.json`. + +Only plugins that integrate third-party services are listed in the Medusa integrations page. If your plugin doesn't integrate a third-party service, you can skip this step. + +```json title="package.json" +{ + "keywords": [ + "medusa-plugin-integration", + "medusa-v2" + ], + // ... +} +``` + +In addition, make sure to use one of the following keywords based on your integration type: + +|Keyword|Description|Example| +|---|---|---| +|\`medusa-plugin-analytics\`|Analytics service integration|Google Analytics| +|\`medusa-plugin-auth\`|Authentication service integration|Auth0| +|\`medusa-plugin-cms\`|CMS service integration|Contentful| +|\`medusa-plugin-notification\`|Notification service integration|Twilio SMS| +|\`medusa-plugin-payment\`|Payment service integration|PayPal| +|\`medusa-plugin-search\`|Search service integration|MeiliSearch| +|\`medusa-plugin-shipping\`|Shipping service integration|DHL| +|\`medusa-plugin-other\`|Other third-party integrations|Sentry| + +### Package Dependencies + +Your plugin project will already have the dependencies mentioned in this section. Unless you made changes to the dependencies, you can skip this section. + +In the `package.json` file you must have the Medusa dependencies as `devDependencies` and `peerDependencies`. In addition, you must have `@swc/core` as a `devDependency`, as it's used by the plugin CLI tools. + +For example, assuming `2.5.0` is the latest Medusa version: + +```json title="package.json" +{ + "devDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/ui": "4.0.4", + "@medusajs/icons": "2.5.0", + "@swc/core": "1.5.7", + }, + "peerDependencies": { + "@medusajs/admin-sdk": "2.5.0", + "@medusajs/cli": "2.5.0", + "@medusajs/framework": "2.5.0", + "@medusajs/test-utils": "2.5.0", + "@medusajs/medusa": "2.5.0", + "@medusajs/ui": "4.0.3", + "@medusajs/icons": "2.5.0", + } +} +``` + +### Package Exports + +Your plugin project will already have the exports mentioned in this section. Unless you made changes to the exports or you created your plugin before [Medusa v2.7.0](https://github.com/medusajs/medusa/releases/tag/v2.7.0), you can skip this section. + +In the `package.json` file, make sure your plugin has the following exports: + +```json title="package.json" +{ + "exports": { + "./package.json": "./package.json", + "./workflows": "./.medusa/server/src/workflows/index.js", + "./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js", + "./providers/*": "./.medusa/server/src/providers/*/index.js", + "./admin": { + "import": "./.medusa/server/src/admin/index.mjs", + "require": "./.medusa/server/src/admin/index.js", + "default": "./.medusa/server/src/admin/index.js" + }, + "./*": "./.medusa/server/src/*.js" + } +} +``` + +Aside from the `./package.json`, `./providers`, and `./admin`, these exports are only a recommendation. You can cherry-pick the files and directories you want to export. + +The plugin exports the following files and directories: + +- `./package.json`: The `package.json` file. Medusa needs to access the `package.json` when registering the plugin. +- `./workflows`: The workflows exported in `./src/workflows/index.ts`. +- `./.medusa/server/src/modules/*`: The definition file of modules. This is useful if you create links to the plugin's modules in the Medusa application. +- `./providers/*`: The definition file of module providers. This is useful if your plugin includes a module provider, allowing you to register the plugin's providers in Medusa applications. Learn more in the [Create Module Providers](#create-module-providers) section. +- `./admin`: The admin extensions exported in `./src/admin/index.ts`. +- `./*`: Any other files in the plugin's `src` directory. + +*** + +## 3. Publish Plugin Locally for Development and Testing + +Medusa's CLI tool provides commands to simplify developing and testing your plugin in a local Medusa application. You start by publishing your plugin in the local package registry, then install it in your Medusa application. You can then watch for changes in the plugin as you develop it. + +### Publish and Install Local Package + +### Prerequisites + +- [Medusa application installed.](https://docs.medusajs.com/learn/installation/index.html.md) + +The first time you create your plugin, you need to publish the package into a local package registry, then install it in your Medusa application. This is a one-time only process. + +To publish the plugin to the local registry, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, navigate to your Medusa application: + +```bash title="Medusa application" +cd ~/path/to/medusa-app +``` + +Make sure to replace `~/path/to/medusa-app` with the path to your Medusa application. + +Then, if your project was created before v2.3.1 of Medusa, make sure to install `yalc` as a development dependency: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm install --save-dev yalc +``` + +After that, run the following Medusa CLI command to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add @myorg/plugin-name +``` + +Make sure to replace `@myorg/plugin-name` with the name of your plugin as specified in `package.json`. Your plugin will be installed from the local package registry into your Medusa application. + +### Register Plugin in Medusa Application + +After installing the plugin, you need to register it in your Medusa application in the configurations defined in `medusa-config.ts`. + +Add the plugin to the `plugins` array in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" highlights={pluginHighlights} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: {}, + }, + ], +}) +``` + +The `plugins` configuration is an array of objects where each object has a `resolve` key whose value is the name of the plugin package. + +#### Pass Module Options through Plugin + +Each plugin configuration also accepts an `options` property, whose value is an object of options to pass to the plugin's modules. + +For example: + +```ts title="medusa-config.ts" highlights={pluginOptionsHighlight} +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "@myorg/plugin-name", + options: { + apiKey: true, + }, + }, + ], +}) +``` + +The `options` property in the plugin configuration is passed to all modules in the plugin. Learn more about module options in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/options/index.html.md). + +### Watch Plugin Changes During Development + +While developing your plugin, you can watch for changes in the plugin and automatically update the plugin in the Medusa application using it. This is the only command you'll continuously need during your plugin development. + +To do that, run the following command in your plugin project: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +This command will: + +- Watch for changes in the plugin. Whenever a file is changed, the plugin is automatically built. +- Publish the plugin changes to the local package registry. This will automatically update the plugin in the Medusa application using it. You can also benefit from real-time HMR updates of admin extensions. + +### Start Medusa Application + +You can start your Medusa application's development server to test out your plugin: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm run dev +``` + +While your Medusa application is running and the plugin is being watched, you can test your plugin while developing it in the Medusa application. + +*** + +## 4. Create Customizations in the Plugin + +You can now build your plugin's customizations. The following guide explains how to build different customizations in your plugin. + +- [Create a module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md) +- [Create a module link](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) +- [Create a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) +- [Add a workflow hook](https://docs.medusajs.com/learn/fundamentals/workflows/add-workflow-hook/index.html.md) +- [Create an API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md) +- [Add a subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) +- [Add a scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) +- [Add an admin widget](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md) +- [Add an admin UI route](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md) + +While building those customizations, you can test them in your Medusa application by [watching the plugin changes](#watch-plugin-changes-during-development) and [starting the Medusa application](#start-medusa-application). + +### Generating Migrations for Modules + +During your development, you may need to generate migrations for modules in your plugin. To do that, first, add the following environment variables in your plugin project: + +```plain title="Plugin project" +DB_USERNAME=postgres +DB_PASSWORD=123... +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=db_name +``` + +You can add these environment variables in a `.env` file in your plugin project. The variables are: + +- `DB_USERNAME`: The username of the PostgreSQL user to connect to the database. +- `DB_PASSWORD`: The password of the PostgreSQL user to connect to the database. +- `DB_HOST`: The host of the PostgreSQL database. Typically, it's `localhost` if you're running the database locally. +- `DB_PORT`: The port of the PostgreSQL database. Typically, it's `5432` if you're running the database locally. +- `DB_NAME`: The name of the PostgreSQL database to connect to. + +Then, run the following command in your plugin project to generate migrations for the modules in your plugin: + +```bash title="Plugin project" +npx medusa plugin:db:generate +``` + +This command generates migrations for all modules in the plugin. + +Finally, run these migrations on the Medusa application that the plugin is installed in using the `db:migrate` command: + +```bash title="Medusa application" +npx medusa db:migrate +``` + +The migrations in your application, including your plugin, will run and update the database. + +### Importing Module Resources + +In the [Prepare Plugin](#2-prepare-plugin) section, you learned about [exported resources](#package-exports) in your plugin. + +These exports allow you to import your plugin resources in your Medusa application, including workflows, links and modules. + +For example, to import the plugin's workflow in your Medusa application: + +`@myorg/plugin-name` is the plugin package's name. + +```ts +import { Workflow1, Workflow2 } from "@myorg/plugin-name/workflows" +import BlogModule from "@myorg/plugin-name/modules/blog" +// import other files created in plugin like ./src/types/blog.ts +import BlogType from "@myorg/plugin-name/types/blog" +``` + +### Create Module Providers + +The [exported resources](#package-exports) also allow you to import module providers in your plugin and register them in the Medusa application's configuration. A module provider is a module that provides the underlying logic or integration related to a commerce or Infrastructure Module. + +For example, assuming your plugin has a [Notification Module Provider](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md) called `my-notification`, you can register it in your Medusa application's configuration like this: + +`@myorg/plugin-name` is the plugin package's name. + +```ts highlights={[["9"]]} title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@myorg/plugin-name/providers/my-notification", + id: "my-notification", + options: { + channels: ["email"], + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +You pass to `resolve` the path to the provider relative to the plugin package. So, in this example, the `my-notification` provider is located in `./src/providers/my-notification/index.ts` of the plugin. + +To learn how to create module providers, refer to the following guides: + +- [File Module Provider](https://docs.medusajs.com/resources/references/file-provider-module/index.html.md) +- [Notification Module Provider](https://docs.medusajs.com/resources/references/notification-provider-module/index.html.md) +- [Auth Module Provider](https://docs.medusajs.com/resources/references/auth/provider/index.html.md) +- [Payment Module Provider](https://docs.medusajs.com/resources/references/payment/provider/index.html.md) +- [Fulfillment Module Provider](https://docs.medusajs.com/resources/references/fulfillment/provider/index.html.md) +- [Tax Module Provider](https://docs.medusajs.com/resources/references/tax/provider/index.html.md) + +*** + +## 5. Publish Plugin to NPM + +Make sure to add the keywords mentioned in the [Package Keywords](#package-keywords) section in your plugin's `package.json` file. + +Medusa's CLI tool provides a command that bundles your plugin to be published to npm. Once you're ready to publish your plugin publicly, run the following command in your plugin project: + +```bash +npx medusa plugin:build +``` + +The command will compile an output in the `.medusa/server` directory. + +You can now publish the plugin to npm using the [NPM CLI tool](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Run the following command to publish the plugin to npm: + +```bash +npm publish +``` + +If you haven't logged in before with your NPM account, you'll be asked to log in first. Then, your package is published publicly to be used in any Medusa application. + +### Install Public Plugin in Medusa Application + +You install a plugin that's published publicly using your package manager. For example: + +```bash npm2yarn +npm install @myorg/plugin-name +``` + +Where `@myorg/plugin-name` is the name of your plugin as published on NPM. + +Then, register the plugin in your Medusa application's configurations as explained in [this section](#register-plugin-in-medusa-application). + +*** + +## Update a Published Plugin + +To update the Medusa dependencies in a plugin, refer to [this documentation](https://docs.medusajs.com/learn/update#update-plugin-project/index.html.md). + +If you've published a plugin and you've made changes to it, you'll have to publish the update to NPM again. + +First, run the following command to change the version of the plugin: + +```bash +npm version +``` + +Where `` indicates the type of version update you’re publishing. For example, it can be `major` or `minor`. Refer to the [npm version documentation](https://docs.npmjs.com/cli/v10/commands/npm-version) for more information. + +Then, re-run the same commands for publishing a plugin: + +```bash +npx medusa plugin:build +npm publish +``` + +This will publish an updated version of your plugin under a new version. + + +# Scheduled Jobs Number of Executions + +In this chapter, you'll learn how to set a limit on the number of times a scheduled job is executed. + +## numberOfExecutions Option + +The export configuration object of the scheduled job accepts an optional property `numberOfExecutions`. Its value is a number indicating how many times the scheduled job can be executed during the Medusa application's runtime. + +For example: + +```ts highlights={highlights} +export default async function myCustomJob() { + console.log("I'll be executed three times only.") +} + +export const config = { + name: "hello-world", + // execute every minute + schedule: "* * * * *", + numberOfExecutions: 3, +} +``` + +The above scheduled job has the `numberOfExecutions` configuration set to `3`. + +So, it'll only execute 3 times, each every minute, then it won't be executed anymore. + +If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. + + # Expose a Workflow Hook In this chapter, you'll learn how to expose a hook in your workflow. @@ -15692,360 +15692,6 @@ Since `then` returns a value different than the step's result, you pass to the ` The second and third parameters are the same as the parameters you previously passed to `when`. -# Workflow Constraints - -This chapter lists constraints of defining a workflow or its steps. - -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. - -This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. - -## Workflow Constraints - -### No Async Functions - -The function passed to `createWorkflow` can’t be an async function: - -```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) { - // ... -}) - -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - // ... -}) -``` - -### No Direct Variable Manipulation - -You can’t directly manipulate variables within the workflow's constructor function. - -Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) - -Instead, use `transform` from the Workflows SDK: - -```ts highlights={highlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const str1 = step1(input) - const str2 = step2(input) - - return new WorkflowResponse({ - message: `${str1}${str2}`, - }) -}) - -// 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}`, - }) - ) - - return new WorkflowResponse(result) -}) -``` - -#### Create Dates in transform - -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. - -Instead, create the date using `transform`. - -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). - -For example: - -```ts highlights={dateHighlights} -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = new Date() - - return new WorkflowResponse({ - today, - }) -}) - -// Do -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const today = transform({}, () => new Date()) - - return new WorkflowResponse({ - today, - }) -}) -``` - -### No If Conditions - -You can't use if-conditions in a workflow. - -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) - -Instead, use when-then from the Workflows SDK: - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - if (input.is_active) { - // perform an action - } -}) - -// 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 - }) -}) -``` - -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). - -### No Conditional Operators - -You can't use conditional operators in a workflow, such as `??` or `||`. - -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) - -Instead, use `transform` to store the desired value in a variable. - -#### Logical Or (||) Alternative - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message || "Hello" -}) - -// 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" - ) -}) -``` - -#### Nullish Coalescing (??) Alternative - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = input.message ?? "Hello" -}) - -// 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" - ) -}) -``` - -#### Double Not (!!) Alternative - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - isActive: !!input.is_active, - }) -}) - -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" - -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const isActive = transform( - { - input, - }, - (data) => !!data.input.is_active - ) - - step1({ - isActive, - }) -}) -``` - -#### Ternary Alternative - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - message: input.is_active ? "active" : "inactive", - }) -}) - -// Do -// other imports... -import { transform } from "@medusajs/framework/workflows-sdk" - -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - const message = transform( - { - input, - }, - (data) => { - return data.input.is_active ? "active" : "inactive" - } - ) - - step1({ - message, - }) -}) -``` - -#### Optional Chaining (?.) Alternative - -```ts -// Don't -const myWorkflow = createWorkflow( - "hello-world", - function (input: WorkflowInput) { - step1({ - name: input.customer?.name, - }) -}) - -// 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, - }) -}) -``` - -### No Try-Catch - -In a workflow, don't use try-catch blocks to handle errors. - -Instead, refer to the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter for alternative ways to handle errors. - -*** - -## Step Constraints - -### Returned Values - -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. - -Values of other types, such as Maps, aren't allowed. - -```ts -// Don't -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - (input, { container }) => { - const myMap = new Map() - - // ... - - return new StepResponse({ - myMap, - }) - } -) - -// Do -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - (input, { container }) => { - const myObj: Record = {} - - // ... - - return new StepResponse({ - myObj, - }) - } -) -``` - - # Error Handling in Workflows In this chapter, you’ll learn about what happens when an error occurs in a workflow, how to disable error throwing in a workflow, and try-catch alternatives in workflow definitions. @@ -16421,79 +16067,359 @@ const workflow = createWorkflow( In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. -# Multiple Step Usage in Workflow +# Workflow Constraints -In this chapter, you'll learn how to use a step multiple times in a workflow. +This chapter lists constraints of defining a workflow or its steps. -## Problem Reusing a Step in a Workflow +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. -In some cases, you may need to use a step multiple times in the same workflow. +This creates restrictions related to variable manipulations, using if-conditions, and other constraints. This chapter lists these constraints and provides their alternatives. -The most common example is using the `useQueryGraphStep` multiple times in a workflow to retrieve multiple unrelated data, such as customers and products. +## Workflow Constraints -Each workflow step must have a unique ID, which is the ID passed as a first parameter when creating the step: +### No Async Functions -```ts -const useQueryGraphStep = createStep( - "use-query-graph" +The function passed to `createWorkflow` can’t be an async function: + +```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) { // ... -) +}) + +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + // ... +}) ``` -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: +### No Direct Variable Manipulation + +You can’t directly manipulate variables within the workflow's constructor function. + +Learn more about why you can't manipulate variables [in this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) + +Instead, use `transform` from the Workflows SDK: + +```ts highlights={highlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const str1 = step1(input) + const str2 = step2(input) + + return new WorkflowResponse({ + message: `${str1}${str2}`, + }) +}) + +// 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}`, + }) + ) + + return new WorkflowResponse(result) +}) +``` + +#### Create Dates in transform + +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. + +Instead, create the date using `transform`. + +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). + +For example: + +```ts highlights={dateHighlights} +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = new Date() + + return new WorkflowResponse({ + today, + }) +}) + +// Do +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const today = transform({}, () => new Date()) + + return new WorkflowResponse({ + today, + }) +}) +``` + +### No If Conditions + +You can't use if-conditions in a workflow. + +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) + +Instead, use when-then from the Workflows SDK: ```ts -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], - }) - - // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }) +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + if (input.is_active) { + // perform an action } -) +}) + +// 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 next section explains how to fix this issue to use the same step multiple times in a workflow. +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). + +### No Conditional Operators + +You can't use conditional operators in a workflow, such as `??` or `||`. + +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) + +Instead, use `transform` to store the desired value in a variable. + +#### Logical Or (||) Alternative + +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message || "Hello" +}) + +// 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" + ) +}) +``` + +#### Nullish Coalescing (??) Alternative + +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = input.message ?? "Hello" +}) + +// 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" + ) +}) +``` + +#### Double Not (!!) Alternative + +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + isActive: !!input.is_active, + }) +}) + +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" + +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const isActive = transform( + { + input, + }, + (data) => !!data.input.is_active + ) + + step1({ + isActive, + }) +}) +``` + +#### Ternary Alternative + +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + message: input.is_active ? "active" : "inactive", + }) +}) + +// Do +// other imports... +import { transform } from "@medusajs/framework/workflows-sdk" + +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + const message = transform( + { + input, + }, + (data) => { + return data.input.is_active ? "active" : "inactive" + } + ) + + step1({ + message, + }) +}) +``` + +#### Optional Chaining (?.) Alternative + +```ts +// Don't +const myWorkflow = createWorkflow( + "hello-world", + function (input: WorkflowInput) { + step1({ + name: input.customer?.name, + }) +}) + +// 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, + }) +}) +``` + +### No Try-Catch + +In a workflow, don't use try-catch blocks to handle errors. + +Instead, refer to the [Error Handling](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md) chapter for alternative ways to handle errors. *** -## How to Use a Step Multiple Times in a Workflow? +## Step Constraints -When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. +### Returned Values -Use the `config` method to change a step's ID for a single execution. +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. -So, this is the correct way to write the example above: +Values of other types, such as Maps, aren't allowed. -```ts highlights={highlights} -const helloWorkflow = createWorkflow( - "hello", - () => { - const { data: products } = useQueryGraphStep({ - entity: "product", - fields: ["id"], +```ts +// Don't +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + (input, { container }) => { + const myMap = new Map() + + // ... + + return new StepResponse({ + myMap, }) + } +) - // ✓ No error occurs, the step has a different ID. - const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: ["id"], - }).config({ name: "fetch-customers" }) +// Do +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + (input, { container }) => { + const myObj: Record = {} + + // ... + + return new StepResponse({ + myObj, + }) } ) ``` -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 first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. - # Long-Running Workflows @@ -16790,59 +16716,6 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions. -# Run Workflow Steps in Parallel - -In this chapter, you’ll learn how to run workflow steps in parallel. - -## parallelize Utility Function - -If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. - -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" -import { - createProductStep, - getProductStep, - createPricesStep, - attachProductToSalesChannelStep, -} from "./steps" - -interface WorkflowInput { - title: string -} - -const myWorkflow = createWorkflow( - "my-workflow", - (input: WorkflowInput) => { - const product = createProductStep(input) - - const [prices, productSalesChannel] = parallelize( - createPricesStep(product), - attachProductToSalesChannelStep(product) - ) - - const refetchedProduct = getProductStep(product.id) - - return new WorkflowResponse(refetchedProduct) - } -) -``` - -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. @@ -16966,6 +16839,133 @@ However, since the long-running workflow runs in the background, you won't recei 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). +# Run Workflow Steps in Parallel + +In this chapter, you’ll learn how to run workflow steps in parallel. + +## parallelize Utility Function + +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. + +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" +import { + createProductStep, + getProductStep, + createPricesStep, + attachProductToSalesChannelStep, +} from "./steps" + +interface WorkflowInput { + title: string +} + +const myWorkflow = createWorkflow( + "my-workflow", + (input: WorkflowInput) => { + const product = createProductStep(input) + + const [prices, productSalesChannel] = parallelize( + createPricesStep(product), + attachProductToSalesChannelStep(product) + ) + + const refetchedProduct = getProductStep(product.id) + + return new WorkflowResponse(refetchedProduct) + } +) +``` + +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`. + + +# Multiple Step Usage in Workflow + +In this chapter, you'll learn how to use a step multiple times in a workflow. + +## Problem Reusing a Step in a Workflow + +In some cases, you may need to use a step multiple times in the same workflow. + +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: + +```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"], + }) + + // ERROR OCCURS HERE: A STEP HAS THE SAME ID AS ANOTHER IN THE WORKFLOW + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }) + } +) +``` + +The next section explains how to fix this issue to use the same step multiple times in a workflow. + +*** + +## How to Use a Step Multiple Times in a Workflow? + +When you execute a step in a workflow, you can chain a `config` method to it to change the step's config. + +Use the `config` method to change a step's ID for a single execution. + +So, this is the correct way to write the example above: + +```ts highlights={highlights} +const helloWorkflow = createWorkflow( + "hello", + () => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: ["id"], + }) + + // ✓ No error occurs, the step has a different ID. + const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: ["id"], + }).config({ name: "fetch-customers" }) + } +) +``` + +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 first `useQueryGraphStep` usage has the ID `use-query-graph`, and the second `useQueryGraphStep` usage has the ID `fetch-customers`. + + # Store Workflow Executions In this chapter, you'll learn how to store workflow executions in the database and access them later. @@ -17440,6 +17440,92 @@ export async function POST(req: MedusaRequest, res: MedusaResponse) { Your hook handler then receives that passed data in the `additional_data` object. +# Workflow Timeout + +In this chapter, you’ll learn how to set a timeout for workflows and steps. + +## What is a Workflow Timeout? + +By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. + +You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. + +### Timeout Doesn't Stop Step Execution + +Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. + +*** + +## Configure Workflow Timeout + +The `createWorkflow` function can accept a configuration object instead of the workflow’s name. + +In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. + +For example: + +```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" +import { + createStep, + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async () => { + // ... + } +) + +const myWorkflow = createWorkflow({ + name: "hello-world", + timeout: 2, // 2 seconds +}, function () { + const str1 = step1() + + return new WorkflowResponse({ + message: str1, + }) +}) + +export default myWorkflow + +``` + +This workflow's executions fail if they run longer than two seconds. + +A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. + +*** + +## Configure Step Timeout + +Alternatively, you can configure the timeout for a step rather than the entire workflow. + +As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. + +The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. + +For example: + +```tsx +const step1 = createStep( + { + name: "step-1", + timeout: 2, // 2 seconds + }, + async () => { + // ... + } +) +``` + +This step's executions fail if they run longer than two seconds. + +A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. + + # Translate Medusa Admin The Medusa Admin supports multiple languages, with the default being English. In this documentation, you'll learn how to contribute to the community by translating the Medusa Admin to a language you're fluent in. @@ -17534,356 +17620,6 @@ export const languages: Language[] = [ Our team will perform a general review on your PR and merge it if no issues are found. The translation will be available in the admin after the next release. -# Workflow Timeout - -In this chapter, you’ll learn how to set a timeout for workflows and steps. - -## What is a Workflow Timeout? - -By default, a workflow doesn’t have a timeout. It continues execution until it’s finished or an error occurs. - -You can configure a workflow’s timeout to indicate how long the workflow can execute. If a workflow's execution time passes the configured timeout, it is failed and an error is thrown. - -### Timeout Doesn't Stop Step Execution - -Configuring a timeout doesn't stop the execution of a step in progress. The timeout only affects the status of the workflow and its result. - -*** - -## Configure Workflow Timeout - -The `createWorkflow` function can accept a configuration object instead of the workflow’s name. - -In the configuration object, you pass a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```ts title="src/workflows/hello-world.ts" highlights={[["16"]]} collapsibleLines="1-13" expandButtonLabel="Show More" -import { - createStep, - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async () => { - // ... - } -) - -const myWorkflow = createWorkflow({ - name: "hello-world", - timeout: 2, // 2 seconds -}, function () { - const str1 = step1() - - return new WorkflowResponse({ - message: str1, - }) -}) - -export default myWorkflow - -``` - -This workflow's executions fail if they run longer than two seconds. - -A workflow’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionTimeoutError`. - -*** - -## Configure Step Timeout - -Alternatively, you can configure the timeout for a step rather than the entire workflow. - -As mentioned in the previous section, the timeout doesn't stop the execution of the step. It only affects the step's status and output. - -The step’s configuration object accepts a `timeout` property, whose value is a number indicating the timeout in seconds. - -For example: - -```tsx -const step1 = createStep( - { - name: "step-1", - timeout: 2, // 2 seconds - }, - async () => { - // ... - } -) -``` - -This step's executions fail if they run longer than two seconds. - -A step’s timeout error is returned in the `errors` property of the workflow’s execution, as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/errors/index.html.md). The error’s name is `TransactionStepTimeoutError`. - - -# Docs Contribution Guidelines - -Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. - -This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). - -## What Can You Contribute? - -You can contribute to the Medusa documentation in the following ways: - -- Fixes to existing content. This includes small fixes like typos, or adding missing information. -- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. - - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. -- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. - -*** - -## Documentation Workspace - -Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. - -The workspace has the following two directories: - -- `apps`: this directory holds the different documentation websites and projects. - - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). - - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). - - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). - - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). -- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. - - `docs-ui` includes the shared React components between the different apps. - - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. - -*** - -## Documentation Content - -All documentation projects are built with Next.js. The content is writtin in MDX files. - -### Medusa Main Docs Content - -The content of the Medusa main docs are under the `www/apps/book/app` directory. - -### Medusa Resources Content - -The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. - -Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. - -### API Reference - -The API reference's content is split into two types: - -1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. -2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. - -### Medusa UI Documentation - -The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. - -The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. - -The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. - -*** - -## Style Guide - -When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). - -*** - -## How to Contribute - -If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). - -If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. - -### Base Branch - -When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. - -Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. - -### Branch Name - -Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. - -### Pull Request Conventions - -When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. - -In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. - -*** - -## Images - -If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. - -*** - -## NPM and Yarn Code Blocks - -If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. - -For example: - -````md -```bash npm2yarn -npm run start -``` -```` - -The code snippet must be written using NPM. - -### Global Option - -When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example: - -```bash npm2yarn -npm install @medusajs/cli -g -``` - -*** - -## Linting with Vale - -Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. - -### Result of Vale PR Checks - -You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix. - -### Run Vale Locally - -If you want to check your work locally, you can do that by: - -1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. -2. Changing to the `www/vale` directory: - -```bash -cd www/vale -``` - -3\. Running the `run-vale` script: - -```bash -# to lint content for the main documentation -./run-vale.sh book/app/learn error resources -# to lint content for the resources documentation -./run-vale.sh resources/app error -# to lint content for the API reference -./run-vale.sh api-reference/markdown error -# to lint content for the Medusa UI documentation -./run-vale.sh ui/src/content/docs error -# to lint content for the user guide -./run-vale.sh user-guide/app error -``` - -{/* TODO need to enable MDX v1 comments first. */} - -{/* ### Linter Exceptions - -If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files: - -```md - - -content that shouldn't be scanned for errors here... - - -``` - -You can also disable specific rules. For example: - -```md - - -Medusa supports Node versions 14 and 16. - - -``` - -If you use this in your PR, you must justify its usage. */} - -*** - -## Linting with ESLint - -Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. - -### Linting Content with ESLint - -Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. - -If you want to check content ESLint errors locally and fix them, you can do that by: - -1\. Install the dependencies in the `www` directory: - -```bash -yarn install -``` - -2\. Run the turbo command in the `www` directory: - -```bash -turbo run lint:content -``` - -This will fix any fixable errors, and show errors that require your action. - -### Linting Code with ESLint - -Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. - -If you want to check code ESLint errors locally and fix them, you can do that by: - -1\. Install the dependencies in the `www` directory: - -```bash -yarn install -``` - -2\. Run the turbo command in the `www` directory: - -```bash -yarn lint -``` - -This will fix any fixable errors, and show errors that require your action. - -{/* TODO need to enable MDX v1 comments first. */} - -{/* ### ESLint Exceptions - -If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: - -~~~md - - -```js -console.log("This block isn't linted") -``` - -```js -console.log("This block is linted") -``` -~~~ - -You can also disable specific rules. For example: - -~~~md - - -```js -console.log("This block can use semicolons"); -``` - -```js -console.log("This block can't use semi colons") -``` -~~~ */} - - # 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. @@ -18654,6 +18390,270 @@ If you don't have a `test:integration:modules` script in `package.json`, refer t This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. +# Docs Contribution Guidelines + +Thank you for your interest in contributing to the documentation! You will be helping the open source community and other developers interested in learning more about Medusa and using it. + +This guide is specific to contributing to the documentation. If you’re interested in contributing to Medusa’s codebase, check out the [contributing guidelines in the Medusa GitHub repository](https://github.com/medusajs/medusa/blob/develop/CONTRIBUTING.md). + +## What Can You Contribute? + +You can contribute to the Medusa documentation in the following ways: + +- Fixes to existing content. This includes small fixes like typos, or adding missing information. +- Additions to the documentation. If you think a documentation page can be useful to other developers, you can contribute by adding it. + - Make sure to open an issue first in the [medusa repository](https://github.com/medusajs/medusa) to confirm that you can add that documentation page. +- Fixes to UI components and tooling. If you find a bug while browsing the documentation, you can contribute by fixing it. + +*** + +## Documentation Workspace + +Medusa's documentation projects are all part of the documentation yarn workspace, which you can find in the [medusa repository](https://github.com/medusajs/medusa) under the `www` directory. + +The workspace has the following two directories: + +- `apps`: this directory holds the different documentation websites and projects. + - `book`: includes the codebase for the [main Medusa documentation](https://docs.medusajs.com//index.html.md). It's built with [Next.js 15](https://nextjs.org/). + - `resources`: includes the codebase for the resources documentation, which powers different sections of the docs such as the [Integrations](https://docs.medusajs.com/resources/integrations/index.html.md) or [How-to & Tutorials](https://docs.medusajs.com/resources/how-to-tutorials/index.html.md) sections. It's built with [Next.js 15](https://nextjs.org/). + - `api-reference`: includes the codebase for the API reference website. It's built with [Next.js 15](https://nextjs.org/). + - `ui`: includes the codebase for the Medusa UI documentation website. It's built with [Next.js 15](https://nextjs.org/). +- `packages`: this directory holds the shared packages and components necessary for the development of the projects in the `apps` directory. + - `docs-ui` includes the shared React components between the different apps. + - `remark-rehype-plugins` includes Remark and Rehype plugins used by the documentation projects. + +*** + +## Documentation Content + +All documentation projects are built with Next.js. The content is writtin in MDX files. + +### Medusa Main Docs Content + +The content of the Medusa main docs are under the `www/apps/book/app` directory. + +### Medusa Resources Content + +The content of all pages under the `/resources` path are under the `www/apps/resources/app` directory. + +Documentation pages under the `www/apps/resources/references` directory are generated automatically from the source code under the `packages/medusa` directory. So, you can't directly make changes to them. Instead, you'll have to make changes to the comments in the original source code. + +### API Reference + +The API reference's content is split into two types: + +1. Static content, which are the content related to getting started, expanding fields, and more. These are located in the `www/apps/api-reference/markdown` directory. They are MDX files. +2. OpenAPI specs that are shown to developers when checking the reference of an API Route. These are generated from OpenApi Spec comments, which are under the `www/utils/generated/oas-output` directory. + +### Medusa UI Documentation + +The content of the Medusa UI documentation are located under the `www/apps/ui/src/content/docs` directory. They are MDX files. + +The UI documentation also shows code examples, which are under the `www/apps/ui/src/examples` directory. + +The UI component props are generated from the source code and placed into the `www/apps/ui/src/specs` directory. To contribute to these props and their comments, check the comments in the source code under the `packages/design-system/ui` directory. + +*** + +## Style Guide + +When you contribute to the documentation content, make sure to follow the [documentation style guide](https://www.notion.so/Style-Guide-Docs-fad86dd1c5f84b48b145e959f36628e0). + +*** + +## How to Contribute + +If you’re fixing errors in an existing documentation page, you can scroll down to the end of the page and click on the “Edit this page” link. You’ll be redirected to the GitHub edit form of that page and you can make edits directly and submit a pull request (PR). + +If you’re adding a new page or contributing to the codebase, fork the repository, create a new branch, and make all changes necessary in your repository. Then, once you’re done, create a PR in the Medusa repository. + +### Base Branch + +When you make an edit to an existing documentation page or fork the repository to make changes to the documentation, create a new branch. + +Documentation contributions always use `develop` as the base branch. Make sure to also open your PR against the `develop` branch. + +### Branch Name + +Make sure that the branch name starts with `docs/`. For example, `docs/fix-services`. Vercel deployed previews are only triggered for branches starting with `docs/`. + +### Pull Request Conventions + +When you create a pull request, prefix the title with `docs:` or `docs(PROJECT_NAME):`, where `PROJECT_NAME` is the name of the documentation project this pull request pertains to. For example, `docs(ui): fix titles`. + +In the body of the PR, explain clearly what the PR does. If the PR solves an issue, use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) with the issue number. For example, “Closes #1333”. + +*** + +## Images + +If you are adding images to a documentation page, you can host the image on [Imgur](https://imgur.com) for free to include it in the PR. Our team will later upload it to our image hosting. + +*** + +## NPM and Yarn Code Blocks + +If you’re adding code blocks that use NPM and Yarn, you must add the `npm2yarn` meta field. + +For example: + +````md +```bash npm2yarn +npm run start +``` +```` + +The code snippet must be written using NPM. + +### Global Option + +When a command uses the global option `-g`, add it at the end of the NPM command to ensure that it’s transformed to a Yarn command properly. For example: + +```bash npm2yarn +npm install @medusajs/cli -g +``` + +*** + +## Linting with Vale + +Medusa uses [Vale](https://vale.sh/) to lint documentation pages and perform checks on incoming PRs into the repository. + +### Result of Vale PR Checks + +You can check the result of running the "lint" action on your PR by clicking the Details link next to it. You can find there all errors that you need to fix. + +### Run Vale Locally + +If you want to check your work locally, you can do that by: + +1. [Installing Vale](https://vale.sh/docs/vale-cli/installation/) on your machine. +2. Changing to the `www/vale` directory: + +```bash +cd www/vale +``` + +3\. Running the `run-vale` script: + +```bash +# to lint content for the main documentation +./run-vale.sh book/app/learn error resources +# to lint content for the resources documentation +./run-vale.sh resources/app error +# to lint content for the API reference +./run-vale.sh api-reference/markdown error +# to lint content for the Medusa UI documentation +./run-vale.sh ui/src/content/docs error +# to lint content for the user guide +./run-vale.sh user-guide/app error +``` + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### Linter Exceptions + +If it's needed to break some style guide rules in a document, you can wrap the parts that the linter shouldn't scan with the following comments in the `md` or `mdx` files: + +```md + + +content that shouldn't be scanned for errors here... + + +``` + +You can also disable specific rules. For example: + +```md + + +Medusa supports Node versions 14 and 16. + + +``` + +If you use this in your PR, you must justify its usage. */} + +*** + +## Linting with ESLint + +Medusa uses ESlint to lint code blocks both in the content and the code base of the documentation apps. + +### Linting Content with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `content-eslint`. + +If you want to check content ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +turbo run lint:content +``` + +This will fix any fixable errors, and show errors that require your action. + +### Linting Code with ESLint + +Each PR runs through a check that lints the code in the content files using ESLint. The action's name is `code-docs-eslint`. + +If you want to check code ESLint errors locally and fix them, you can do that by: + +1\. Install the dependencies in the `www` directory: + +```bash +yarn install +``` + +2\. Run the turbo command in the `www` directory: + +```bash +yarn lint +``` + +This will fix any fixable errors, and show errors that require your action. + +{/* TODO need to enable MDX v1 comments first. */} + +{/* ### ESLint Exceptions + +If some code blocks have errors that can't or shouldn't be fixed, you can add the following command before the code block: + +~~~md + + +```js +console.log("This block isn't linted") +``` + +```js +console.log("This block is linted") +``` +~~~ + +You can also disable specific rules. For example: + +~~~md + + +```js +console.log("This block can use semicolons"); +``` + +```js +console.log("This block can't use semi colons") +``` +~~~ */} + + # Commerce Modules In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules. @@ -19114,27 +19114,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Inventory Module +# Currency Module -In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. -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. +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). -## Inventory Features +## Currency 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. +- [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 Inventory Module +## 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. @@ -19142,45 +19139,46 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-inventory-item.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 createInventoryItemStep = createStep( - "create-inventory-item", +const retrieveCurrencyStep = createStep( + "retrieve-currency", async ({}, { container }) => { - const inventoryModuleService = container.resolve(Modules.INVENTORY) + const currencyModuleService = container.resolve(Modules.CURRENCY) - const inventoryItem = await inventoryModuleService.createInventoryItems({ - sku: "SHIRT", - title: "Green Medusa Shirt", - requires_shipping: true, - }) + const currency = await currencyModuleService + .retrieveCurrency("usd") - return new StepResponse({ inventoryItem }, inventoryItem.id) - }, - async (inventoryItemId, { container }) => { - if (!inventoryItemId) { - return - } - const inventoryModuleService = container.resolve(Modules.INVENTORY) - - await inventoryModuleService.deleteInventoryItems([inventoryItemId]) + return new StepResponse({ currency }) } ) -export const createInventoryItemWorkflow = createWorkflow( - "create-inventory-item-workflow", - () => { - const { inventoryItem } = createInventoryItemStep() +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({ - inventoryItem, + formattedPrice, }) } ) @@ -19190,19 +19188,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 { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createInventoryItemWorkflow(req.scope) - .run() + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) res.send(result) } @@ -19210,19 +19210,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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createInventoryItemWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -19234,15 +19236,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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createInventoryItemWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -19425,6 +19429,291 @@ The Fulfillment Module accepts options for further configurations. Refer to [thi *** +# Customer Module + +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. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. + +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). + +## Customer Features + +- [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 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. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-customer.ts" highlights={highlights} +import { + 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]) + } +) + +export const createCustomerWorkflow = createWorkflow( + "create-customer", + () => { + const { customer } = createCustomerStep() + + return new WorkflowResponse({ + customer, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createCustomerWorkflow } from "../../workflows/create-customer" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCustomerWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCustomerWorkflow(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 { createCustomerWorkflow } from "../workflows/create-customer" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCustomerWorkflow(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). + +*** + + +# Inventory Module + +In this section of the documentation, you will find resources to learn more about the Inventory Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/inventory/index.html.md) to learn how to manage inventory and related features using the dashboard. + +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). + +## 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 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. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-inventory-item.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} 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]) + } +) + +export const createInventoryItemWorkflow = createWorkflow( + "create-inventory-item-workflow", + () => { + const { inventoryItem } = createInventoryItemStep() + + return new WorkflowResponse({ + inventoryItem, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createInventoryItemWorkflow } from "../../workflows/create-inventory-item" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createInventoryItemWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createInventoryItemWorkflow(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 { createInventoryItemWorkflow } from "../workflows/create-inventory-item" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createInventoryItemWorkflow(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). + +*** + + # Order Module In this section of the documentation, you will find resources to learn more about the Order Module and how to use it in your application. @@ -19581,450 +19870,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Payment Module - -In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. - -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). - -## Payment 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. -- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party 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 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. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createPaymentCollectionStep = createStep( - "create-payment-collection", - async ({}, { container }) => { - const paymentModuleService = container.resolve(Modules.PAYMENT) - - const paymentCollection = await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) - - return new StepResponse({ paymentCollection }, paymentCollection.id) - }, - async (paymentCollectionId, { container }) => { - if (!paymentCollectionId) { - return - } - const paymentModuleService = container.resolve(Modules.PAYMENT) - - await paymentModuleService.deletePaymentCollections([paymentCollectionId]) - } -) - -export const createPaymentCollectionWorkflow = createWorkflow( - "create-payment-collection", - () => { - const { paymentCollection } = createPaymentCollectionStep() - - return new WorkflowResponse({ - paymentCollection, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createPaymentCollectionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createPaymentCollectionWorkflow(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 { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createPaymentCollectionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure 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. - -*** - - -# Currency Module - -In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store's currencies using the dashboard. - -Medusa has currency related features available out-of-the-box through the Currency Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Currency Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Currency Features - -- [Currency Management and Retrieval](https://docs.medusajs.com/references/currency/listAndCountCurrencies/index.html.md): This module adds all common currencies to your application and allows you to retrieve them. -- [Support Currencies in Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/links-to-other-modules/index.html.md): Other Commerce Modules use currency codes in their data models or operations. Use the Currency Module to retrieve a currency code and its details. - -*** - -## How to Use the Currency Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/retrieve-price-with-currency.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const retrieveCurrencyStep = createStep( - "retrieve-currency", - async ({}, { container }) => { - const currencyModuleService = container.resolve(Modules.CURRENCY) - - const currency = await currencyModuleService - .retrieveCurrency("usd") - - return new StepResponse({ currency }) - } -) - -type Input = { - price: number -} - -export const retrievePriceWithCurrency = createWorkflow( - "create-currency", - (input: Input) => { - const { currency } = retrieveCurrencyStep() - - const formattedPrice = transform({ - input, - currency, - }, (data) => { - return `${data.currency.symbol}${data.input.price}` - }) - - return new WorkflowResponse({ - formattedPrice, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await retrievePriceWithCurrency(req.scope) - .run({ - price: 10, - }) - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"], ["13"], ["14"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"], ["9"], ["10"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await retrievePriceWithCurrency(container) - .run({ - price: 10, - }) - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Customer Module - -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. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers and groups using the dashboard. - -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). - -## Customer Features - -- [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 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. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-customer.ts" highlights={highlights} -import { - 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]) - } -) - -export const createCustomerWorkflow = createWorkflow( - "create-customer", - () => { - const { customer } = createCustomerStep() - - return new WorkflowResponse({ - customer, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createCustomerWorkflow } from "../../workflows/create-customer" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCustomerWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCustomerWorkflow(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 { createCustomerWorkflow } from "../workflows/create-customer" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCustomerWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - # Pricing Module In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. @@ -20334,6 +20179,161 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Payment Module + +In this section of the documentation, you will find resources to learn more about the Payment Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/payments/index.html.md) to learn how to manage order payments using the dashboard. + +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). + +## Payment 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. +- [Saved Payment Methods](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md): Save payment methods for customers in third-party 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 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. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-payment-collection.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createPaymentCollectionStep = createStep( + "create-payment-collection", + async ({}, { container }) => { + const paymentModuleService = container.resolve(Modules.PAYMENT) + + const paymentCollection = await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) + + return new StepResponse({ paymentCollection }, paymentCollection.id) + }, + async (paymentCollectionId, { container }) => { + if (!paymentCollectionId) { + return + } + const paymentModuleService = container.resolve(Modules.PAYMENT) + + await paymentModuleService.deletePaymentCollections([paymentCollectionId]) + } +) + +export const createPaymentCollectionWorkflow = createWorkflow( + "create-payment-collection", + () => { + const { paymentCollection } = createPaymentCollectionStep() + + return new WorkflowResponse({ + paymentCollection, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createPaymentCollectionWorkflow } from "../../workflows/create-payment-collection" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPaymentCollectionWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPaymentCollectionWorkflow(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 { createPaymentCollectionWorkflow } from "../workflows/create-payment-collection" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPaymentCollectionWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure 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 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. @@ -20625,24 +20625,26 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Stock Location Module +# Tax 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 Tax Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. -Medusa has 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 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). -## Stock Location Features +## Tax 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. +- [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. +- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region. *** -## How to Use Stock Location Module's Service +## How to Use Tax Module's Service In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -20650,7 +20652,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-tax-region.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -20659,33 +20661,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStockLocationStep = createStep( - "create-stock-location", +const createTaxRegionStep = createStep( + "create-tax-region", async ({}, { container }) => { - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const taxModuleService = container.resolve(Modules.TAX) - const stockLocation = await stockLocationModuleService.createStockLocations({ - name: "Warehouse 1", + const taxRegion = await taxModuleService.createTaxRegions({ + country_code: "us", }) - return new StepResponse({ stockLocation }, stockLocation.id) + return new StepResponse({ taxRegion }, taxRegion.id) }, - async (stockLocationId, { container }) => { - if (!stockLocationId) { + async (taxRegionId, { container }) => { + if (!taxRegionId) { return } - const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + const taxModuleService = container.resolve(Modules.TAX) - await stockLocationModuleService.deleteStockLocations([stockLocationId]) + await taxModuleService.deleteTaxRegions([taxRegionId]) } ) -export const createStockLocationWorkflow = createWorkflow( - "create-stock-location", +export const createTaxRegionWorkflow = createWorkflow( + "create-tax-region", () => { - const { stockLocation } = createStockLocationStep() + const { taxRegion } = createTaxRegionStep() - return new WorkflowResponse({ stockLocation }) + return new WorkflowResponse({ taxRegion }) } ) ``` @@ -20699,13 +20701,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStockLocationWorkflow } from "../../workflows/create-stock-location" +import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStockLocationWorkflow(req.scope) + const { result } = await createTaxRegionWorkflow(req.scope) .run() res.send(result) @@ -20719,13 +20721,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStockLocationWorkflow } from "../workflows/create-stock-location" +import { createTaxRegionWorkflow } from "../workflows/create-tax-region" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createTaxRegionWorkflow(container) .run() console.log(result) @@ -20740,12 +20742,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 { createTaxRegionWorkflow } from "../workflows/create-tax-region" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStockLocationWorkflow(container) + const { result } = await createTaxRegionWorkflow(container) .run() console.log(result) @@ -20761,6 +20763,12 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +## Configure Tax Module + +The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. + +*** + # Sales Channel Module @@ -20922,24 +20930,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Store Module +# Stock Location 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. +In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/index.html.md) to learn how to manage stock locations using the dashboard. -Medusa has 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 has stock location related features available out-of-the-box through the Stock Location Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Stock Location Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Store Features +## Stock Location Features -- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. -- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. +- [Stock Location Management](https://docs.medusajs.com/references/stock-location-next/models/index.html.md): Store and manage stock locations. Medusa links stock locations with data models of other modules that require a location, such as the [Inventory Module's InventoryLevel](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/links-to-other-modules/index.html.md). +- [Address Management](https://docs.medusajs.com/references/stock-location-next/models/StockLocationAddress/index.html.md): Manage the address of each stock location. *** -## How to Use Store Module's Service +## How to Use Stock Location Module's Service In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -20947,7 +20955,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-stock-location.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -20956,37 +20964,33 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createStoreStep = createStep( - "create-store", +const createStockLocationStep = createStep( + "create-stock-location", async ({}, { container }) => { - const storeModuleService = container.resolve(Modules.STORE) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) - const store = await storeModuleService.createStores({ - name: "My Store", - supported_currencies: [{ - currency_code: "usd", - is_default: true, - }], + const stockLocation = await stockLocationModuleService.createStockLocations({ + name: "Warehouse 1", }) - return new StepResponse({ store }, store.id) + return new StepResponse({ stockLocation }, stockLocation.id) }, - async (storeId, { container }) => { - if(!storeId) { + async (stockLocationId, { container }) => { + if (!stockLocationId) { return } - const storeModuleService = container.resolve(Modules.STORE) - - await storeModuleService.deleteStores([storeId]) + const stockLocationModuleService = container.resolve(Modules.STOCK_LOCATION) + + await stockLocationModuleService.deleteStockLocations([stockLocationId]) } ) -export const createStoreWorkflow = createWorkflow( - "create-store", +export const createStockLocationWorkflow = createWorkflow( + "create-stock-location", () => { - const { store } = createStoreStep() + const { stockLocation } = createStockLocationStep() - return new WorkflowResponse({ store }) + return new WorkflowResponse({ stockLocation }) } ) ``` @@ -21000,13 +21004,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createStoreWorkflow } from "../../workflows/create-store" +import { createStockLocationWorkflow } from "../../workflows/create-stock-location" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createStoreWorkflow(req.scope) + const { result } = await createStockLocationWorkflow(req.scope) .run() res.send(result) @@ -21020,13 +21024,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createStoreWorkflow } from "../workflows/create-store" +import { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createStoreWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -21041,12 +21045,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 { createStockLocationWorkflow } from "../workflows/create-stock-location" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createStoreWorkflow(container) + const { result } = await createStockLocationWorkflow(container) .run() console.log(result) @@ -21063,151 +21067,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Tax Module - -In this section of the documentation, you will find resources to learn more about the Tax Module and how to use it in your application. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -Medusa has tax related features available out-of-the-box through the Tax Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Tax Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Tax Features - -- [Tax Settings Per Region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-region/index.html.md): Set different tax settings for each tax region. -- [Tax Rates and Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-rates-and-rules/index.html.md): Manage each region's default tax rates and override them with conditioned tax rates. -- [Retrieve Tax Lines for carts and orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md): Calculate and retrieve the tax lines of a cart or order's line items and shipping methods with tax providers. -- [Custom Tax Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md): Create custom tax providers to calculate tax lines differently for each tax region. - -*** - -## How to Use Tax Module's Service - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-tax-region.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createTaxRegionStep = createStep( - "create-tax-region", - async ({}, { container }) => { - const taxModuleService = container.resolve(Modules.TAX) - - const taxRegion = await taxModuleService.createTaxRegions({ - country_code: "us", - }) - - return new StepResponse({ taxRegion }, taxRegion.id) - }, - async (taxRegionId, { container }) => { - if (!taxRegionId) { - return - } - const taxModuleService = container.resolve(Modules.TAX) - - await taxModuleService.deleteTaxRegions([taxRegionId]) - } -) - -export const createTaxRegionWorkflow = createWorkflow( - "create-tax-region", - () => { - const { taxRegion } = createTaxRegionStep() - - return new WorkflowResponse({ taxRegion }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createTaxRegionWorkflow } from "../../workflows/create-tax-region" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createTaxRegionWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createTaxRegionWorkflow } from "../workflows/create-tax-region" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createTaxRegionWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Tax Module - -The Tax Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) for details on the module's options. - -*** - - # User Module In this section of the documentation, you will find resources to learn more about the User Module and how to use it in your application. @@ -21355,33 +21214,146 @@ The User Module accepts options for further configurations. Refer to [this docum *** -# API Key Concepts +# Store Module -In this document, you’ll learn about the different types of API keys, their expiration and verification. +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. -## API Key Types +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/store/index.html.md) to learn how to manage your store using the dashboard. -There are two types of API keys: +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. -- `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. +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -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). +## Store Features + +- [Store Management](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create and manage stores in your application. +- [Multi-Tenancy Support](https://docs.medusajs.com/references/store/models/Store/index.html.md): Create multiple stores, each having its own configurations. *** -## API Key Expiration +## How to Use Store Module's Service -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). +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 associated token is no longer usable or verifiable. +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-store.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createStoreStep = createStep( + "create-store", + async ({}, { container }) => { + const storeModuleService = container.resolve(Modules.STORE) + + const store = await storeModuleService.createStores({ + name: "My Store", + supported_currencies: [{ + currency_code: "usd", + is_default: true, + }], + }) + + return new StepResponse({ store }, store.id) + }, + async (storeId, { container }) => { + if(!storeId) { + return + } + const storeModuleService = container.resolve(Modules.STORE) + + await storeModuleService.deleteStores([storeId]) + } +) + +export const createStoreWorkflow = createWorkflow( + "create-store", + () => { + const { store } = createStoreStep() + + return new WorkflowResponse({ store }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createStoreWorkflow } from "../../workflows/create-store" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createStoreWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createStoreWorkflow } from "../workflows/create-store" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createStoreWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createStoreWorkflow } from "../workflows/create-store" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createStoreWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). *** -## Token Verification - -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. - # Links between API Key Module and Other Modules @@ -21481,6 +21453,58 @@ createRemoteLinkStep({ ``` +# Auth Module Provider + +In this guide, you’ll learn about the Auth Module Provider and how it's used. + +## What is 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) + +*** + +## How to Create an Auth Module Provider? + +An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`. + +The module can have multiple auth provider services, where each is registered as a separate auth provider. + +Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider. + +*** + +## 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/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/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. + + # 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. @@ -21683,471 +21707,6 @@ 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 Module Provider - -In this guide, you’ll learn about the Auth Module Provider and how it's used. - -## What is 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) - -*** - -## How to Create an Auth Module Provider? - -An Auth Module Provider is a module whose service extends the `AbstractAuthModuleProvider` imported from `@medusajs/framework/utils`. - -The module can have multiple auth provider services, where each is registered as a separate auth provider. - -Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide to learn how to create an Auth Module Provider. - -*** - -## 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/docs/learn/configurations/medusa-config#httpauthMethodsPerActor/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. - - -# 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. - -These routes are added by Medusa's HTTP layer, not the Auth Module. - -## Types of Authentication Flows - -### 1. Basic Authentication Flow - -This authentication flow doesn't require validation with third-party services. - -[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). - -The steps are: - -![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) - -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). - - 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. - -To handle errors related to existing identities, refer to [this section](#handling-existing-identities). - -### 2. Third-Party Service Authenticate Flow - -This authentication flow authenticates the user with a third-party service, such as Google. - -[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). - -It requires the following steps: - -![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) - -1. Authenticate the user with the [Auth Route](#login-route). -2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. -3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. -4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. -5. If the callback validation is successful, the frontend receives the authentication token. -6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). - - 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). - - 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. - -*** - -## Register Route - -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. - -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) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -### Request Body Parameters - -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - -For example, the EmailPass provider requires an `email` and `password` fields in the request body. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -### Handling Existing Identities - -An auth identity with the same email may already exist in Medusa. This can happen if: - -- Another actor type is using that email. For example, an admin user is trying to register as a customer. -- The same email belongs to a record of the same actor type. For example, another customer has the same email. - -In these scenarios, the Register Route will return an error instead of a token: - -```json -{ - "type": "unauthorized", - "message": "Identity with email already exists" -} -``` - -To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. - -Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: - -```json -{ - "type": "unauthorized", - "message": "Invalid email or password" -} -``` - -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) to send authenticated requests. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers} --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com" - // ... -}' -``` - -For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -### Request Body Parameters - -This route accepts in the request body the data that the specified authentication provider requires to handle authentication. - -For example, the EmailPass provider requires an `email` and `password` fields in the request body. - -#### Overriding Callback URL - -For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. - -This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -If the authentication requires more action with a third-party service, you'll receive a `location` property: - -```json -{ - "location": "https://..." -} -``` - -Redirect to that URL in the frontend to continue the authentication process with the third-party service. - -[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). - -*** - -## Validate Callback Route - -The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 -``` - -Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. - -### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `google`. - -### Query Parameters - -This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. - -### Response Fields - -If the authentication is successful, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -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). - -*** - -## Refresh Token Route - -The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. - -It requires the user's JWT token that they received from the authentication or callback routes. - -```bash -curl -X POST http://localhost:9000/auth/token/refresh \ --H 'Authorization: Bearer {token}' -``` - -### Response Fields - -If the token was refreshed successfully, you'll receive a `token` field in the response body object: - -```json -{ - "token": "..." -} -``` - -Use that token in the header of subsequent requests to send authenticated requests. - -*** - -## Reset Password Routes - -To reset a user's password: - -1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). - - The API route emits the `auth.password_reset` event, passing the token in the payload. - - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. -2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. - - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. - -[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) - -### Generate Reset Password Token Route - -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password --H 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "Whitney_Schultz@gmail.com" -}' -``` - -This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. - -#### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -#### Request Body Parameters - -This route accepts in the request body an object having the following property: - -- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. - -#### Response Fields - -If the authentication is successful, the request returns a `201` response code. - -### Reset Password Route - -The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. - -```bash -curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "email": "Whitney_Schultz@gmail.com", - "password": "supersecret" -}' -``` - -This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. - -#### Path Parameters - -Its path parameters are: - -- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. -- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. - -#### Pass Token in Authorization Header - -Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. - -In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. - -### Request Body Parameters - -This route accepts in the request body an object that has the data necessary for the provider to update the user's password. - -For the `emailpass` provider, you must pass the following properties: - -- `email`: The user's email. -- `password`: The new password. - -### Response Fields - -If the authentication is successful, the request returns an object with a `success` property set to `true`: - -```json -{ - "success": "true" -} -``` - - # How to Create an Actor Type In this document, learn how to create an actor type and authenticate its associated data model. @@ -22547,6 +22106,447 @@ In the workflow, you: You can use this workflow when deleting a manager, such as in an API route. +# 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. + +These routes are added by Medusa's HTTP layer, not the Auth Module. + +## Types of Authentication Flows + +### 1. Basic Authentication Flow + +This authentication flow doesn't require validation with third-party services. + +[How to register customer in storefront using basic authentication flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md). + +The steps are: + +![Diagram showcasing the basic authentication flow between the frontend and the Medusa application](https://res.cloudinary.com/dza7lstvk/image/upload/v1725539370/Medusa%20Resources/basic-auth-routes_pgpjch.jpg) + +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). + - 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. + +To handle errors related to existing identities, refer to [this section](#handling-existing-identities). + +### 2. Third-Party Service Authenticate Flow + +This authentication flow authenticates the user with a third-party service, such as Google. + +[How to authenticate customer with a third-party provider in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). + +It requires the following steps: + +![Diagram showcasing the authentication flow between the frontend, Medusa application, and third-party service](https://res.cloudinary.com/dza7lstvk/image/upload/v1725528159/Medusa%20Resources/Third_Party_Auth_tvf4ng.jpg) + +1. Authenticate the user with the [Auth Route](#login-route). +2. The auth route returns a URL to authenticate with third-party service, such as login with Google. The frontend (such as a storefront), when it receives a `location` property in the response, must redirect to the returned location. +3. Once the authentication with the third-party service finishes, it redirects back to the frontend with a `code` query parameter. So, make sure your third-party service is configured to redirect to your frontend page after successful authentication. +4. The frontend sends a request to the [Validate Callback Route](#validate-callback-route) passing it the query parameters received from the third-party service, such as the `code` and `state` query parameters. +5. If the callback validation is successful, the frontend receives the authentication token. +6. Decode the received token in the frontend using tools like [react-jwt](https://www.npmjs.com/package/react-jwt). + - 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). + - 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. + +*** + +## Register Route + +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/register` that creates an auth identity for an actor type, such as a `customer`. It returns a JWT token that you pass to an API route that creates the user. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/register +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` + +This API route is useful for providers like `emailpass` that uses custom logic to authenticate a user. For authentication providers that authenticate with third-party services, such as Google, use the [Auth Route](#login-route) instead. + +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) to create the customer, passing the [JWT token in the header](https://docs.medusajs.com/api/store#authentication). + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +### Request Body Parameters + +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. + +For example, the EmailPass provider requires an `email` and `password` fields in the request body. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +### Handling Existing Identities + +An auth identity with the same email may already exist in Medusa. This can happen if: + +- Another actor type is using that email. For example, an admin user is trying to register as a customer. +- The same email belongs to a record of the same actor type. For example, another customer has the same email. + +In these scenarios, the Register Route will return an error instead of a token: + +```json +{ + "type": "unauthorized", + "message": "Identity with email already exists" +} +``` + +To handle these scenarios, you can use the [Login Route](#login-route) to validate that the email and password match the existing identity. If so, you can allow the admin user, for example, to register as a customer. + +Otherwise, if the email and password don't match the existing identity, such as when the email belongs to another customer, the [Login Route](#login-route) returns an error: + +```json +{ + "type": "unauthorized", + "message": "Invalid email or password" +} +``` + +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) to send authenticated requests. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers} +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com" + // ... +}' +``` + +For example, if you're authenticating a customer, you send a request to `/auth/customer/emailpass`. + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +### Request Body Parameters + +This route accepts in the request body the data that the specified authentication provider requires to handle authentication. + +For example, the EmailPass provider requires an `email` and `password` fields in the request body. + +#### Overriding Callback URL + +For the [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md) and [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) providers, you can pass a `callback_url` body parameter that overrides the `callbackUrl` set in the provider's configurations. + +This is useful if you want to redirect the user to a different URL after authentication based on their actor type. For example, you can set different `callback_url` for admin users and customers. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +If the authentication requires more action with a third-party service, you'll receive a `location` property: + +```json +{ + "location": "https://..." +} +``` + +Redirect to that URL in the frontend to continue the authentication process with the third-party service. + +[How to login Customers using the authentication route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/login/index.html.md). + +*** + +## Validate Callback Route + +The Medusa application defines an API route at `/auth/{actor_type}/{provider}/callback` that's useful for validating the authentication callback or redirect from third-party services like Google. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/callback?code=123&state=456 +``` + +Refer to the [third-party authentication flow](#2-third-party-service-authenticate-flow) section to see how this route fits into the authentication flow. + +### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `google`. + +### Query Parameters + +This route accepts all the query parameters that the third-party service sends to the frontend after the user completes the authentication process, such as the `code` and `state` query parameters. + +### Response Fields + +If the authentication is successful, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +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). + +*** + +## Refresh Token Route + +The Medusa application defines an API route at `/auth/token/refresh` that's useful after authenticating a user with a third-party service to populate the user's token with their new information. + +It requires the user's JWT token that they received from the authentication or callback routes. + +```bash +curl -X POST http://localhost:9000/auth/token/refresh \ +-H 'Authorization: Bearer {token}' +``` + +### Response Fields + +If the token was refreshed successfully, you'll receive a `token` field in the response body object: + +```json +{ + "token": "..." +} +``` + +Use that token in the header of subsequent requests to send authenticated requests. + +*** + +## Reset Password Routes + +To reset a user's password: + +1. Generate a token using the [Generate Reset Password Token API route](#generate-reset-password-token-route). + - The API route emits the `auth.password_reset` event, passing the token in the payload. + - You can create a subscriber, as seen in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/reset-password/index.html.md), that listens to the event and send a notification to the user. +2. Pass the token to the [Reset Password API route](#reset-password-route) to reset the password. + - The URL in the user's notification should direct them to a frontend URL, which sends a request to this route. + +[Storefront Development: How to Reset a Customer's Password.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) + +### Generate Reset Password Token Route + +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/reset-password` that emits the `auth.password_reset` event, passing the token in the payload. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/reset-password +-H 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "Whitney_Schultz@gmail.com" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for authentication. + +#### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +#### Request Body Parameters + +This route accepts in the request body an object having the following property: + +- `identifier`: The user's identifier in the specified auth provider. For example, for the `emailpass` auth provider, you pass the user's email. + +#### Response Fields + +If the authentication is successful, the request returns a `201` response code. + +### Reset Password Route + +The Medusa application defines an API route at `/auth/{actor_type}/{auth_provider}/update` that accepts a token and, if valid, updates the user's password. + +```bash +curl -X POST http://localhost:9000/auth/{actor_type}/{providers}/update +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "email": "Whitney_Schultz@gmail.com", + "password": "supersecret" +}' +``` + +This API route is useful for providers like `emailpass` that store a user's password and use it for logging them in. + +#### Path Parameters + +Its path parameters are: + +- `{actor_type}`: the actor type of the user you're authenticating. For example, `customer`. +- `{provider}`: the auth provider to handle the authentication. For example, `emailpass`. + +#### Pass Token in Authorization Header + +Before [Medusa v2.6](https://github.com/medusajs/medusa/releases/tag/v2.6), you passed the token as a query parameter. Now, you must pass it in the `Authorization` header. + +In the request's authorization header, you must pass the token generated using the [Generate Reset Password Token route](#generate-reset-password-token-route). You pass it as a bearer token. + +### Request Body Parameters + +This route accepts in the request body an object that has the data necessary for the provider to update the user's password. + +For the `emailpass` provider, you must pass the following properties: + +- `email`: The user's email. +- `password`: The new password. + +### Response Fields + +If the authentication is successful, the request returns an object with a `success` property set to `true`: + +```json +{ + "success": "true" +} +``` + + +# API Key Concepts + +In this document, you’ll learn about the different types of API keys, their expiration and verification. + +## API Key Types + +There are two types of API keys: + +- `publishable`: A public key used in client applications, such as a storefront. +- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. + +The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). + +*** + +## API Key Expiration + +An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). + +The associated token is no longer usable or verifiable. + +*** + +## Token Verification + +To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. + + +# 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). + + # Auth Module Options In this document, you'll learn about the options of the Auth Module. @@ -22613,6 +22613,43 @@ The Medusa application's configuration accept an `authMethodsPerActor` configura Learn more about the `authMethodsPerActor` configuration in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers#configure-allowed-auth-providers-of-actor-types/index.html.md). +# Cart Concepts + +In this document, you’ll get an overview of the main concepts of a cart. + +## Shipping and Billing Addresses + +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). + +![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) + +*** + +## 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. + +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 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 Methods + +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. + +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 + +After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. + +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. + + # How to Handle Password Reset Token Event In this guide, you'll learn how to handle the `auth.password_reset` event, which is emitted when a request is sent to the [Generate Reset Password Token API route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#generate-reset-password-token-route/index.html.md). @@ -22727,43 +22764,6 @@ 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) -# Cart Concepts - -In this document, you’ll get an overview of the main concepts of a cart. - -## Shipping and Billing Addresses - -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). - -![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) - -*** - -## 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. - -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 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 Methods - -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. - -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 - -After an order is placed, you can use a third-party fulfillment provider to fulfill its shipments. - -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. @@ -23396,6 +23396,913 @@ await cartModuleService.setLineItemTaxLines( ``` +# Fulfillment Concepts + +In this document, you’ll learn about some basic fulfillment concepts. + +## Fulfillment Set + +A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. + +A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. + +```ts +const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( + [ + { + name: "Shipping", + type: "shipping", + }, + { + name: "Pick-up", + type: "pick-up", + }, + ] +) +``` + +*** + +## Service Zone + +A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. + +A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. + +![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) + +A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +*** + +## Shipping Profile + +A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. + +A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. + + +# Fulfillment Module Provider + +In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. + +## What is 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). + +![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) + +*** + +## Default Fulfillment Provider + +Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. + +This provider is installed by default in your application and you can use it to fulfill items manually. + +The identifier of the manual fulfillment provider is `fp_manual_manual`. + +*** + +## How to Create a Custom Fulfillment Provider? + +A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. + +The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. + +Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. + +{/* TODO add link to user guide */} + +After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. + +*** + +## How are Fulfillment Providers Registered? + +### Configure Fulfillment Module's Providers + +The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. + +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. + +### Registration on Application Start + +When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. + +For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. + +### FulfillmentProvider Data Model + +A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. + +This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. + +![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) + +The `FulfillmentProvider` data model has the following properties: + +- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. + - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. + +### How to Remove a Fulfillment Provider? + +You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. + +Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. + + +# Item Fulfillment + +In this document, you’ll learn about the concepts of item fulfillment. + +## Fulfillment Data Model + +A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). + +*** + +## Fulfillment Processing by a Fulfillment Provider + +A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. + +The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. + +![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) + +*** + +## data Property + +The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. + +For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. + +*** + +## Fulfillment Items + +A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. + +The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. + +![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) + +*** + +## Fulfillment Label + +Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. + +*** + +## Fulfillment Status + +The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: + +- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. +- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. +- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. + + +# Links between Currency Module and Other Modules + +This document showcases the module links defined between the Currency Module and other Commerce Modules. + +## Summary + +The Currency Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|StoreCurrency|Currency|Read-only - has one|Learn more| + +*** + +## Store Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. + +### Retrieve with Query + +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: + +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies[0].currency +``` + + +# Fulfillment Module Options + +In this document, you'll learn about the options of the Fulfillment Module. + +## providers + +The `providers` option is an array of fulfillment module providers. + +When the Medusa application starts, these providers are registered and can be used to process fulfillments. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/fulfillment", + options: { + providers: [ + { + resolve: `@medusajs/medusa/fulfillment-manual`, + id: "manual", + options: { + // provider options... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Shipping Option + +In this document, you’ll learn about shipping options and their rules. + +## What’s a Shipping Option? + +A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. + +When the customer places their order, they choose a shipping option to be used to fulfill their items. + +A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). + +*** + +## Service Zone Restrictions + +A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. + +For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. + +![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) + +Service zones can be more restrictive, such as restricting to certain cities or province codes. + +The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). + +![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) + +*** + +## Shipping Option Rules + +You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. + +You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. + +These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: + +- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. +- `operator`: The operator used in the condition. For example: + - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. + - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. + - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). +- `value`: One or more values. + +![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) + +A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. + +![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) + +*** + +## Shipping Profile and Types + +A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). + +A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. + +*** + +## data Property + +When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. + +The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. + + +# Links between Fulfillment Module and Other Modules + +This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. + +## Summary + +The Fulfillment Module has the following links to other modules: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Order|Fulfillment|Stored - one-to-many|Learn more| +|Return|Fulfillment|Stored - one-to-many|Learn more| +|PriceSet|ShippingOption|Stored - many-to-one|Learn more| +|Product|ShippingProfile|Stored - many-to-one|Learn more| +|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| +|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| + +*** + +## Order Module + +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. + +Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) + +A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. + +![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) + +### Retrieve with Query + +To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +To retrieve the return, pass `return.*` in `fields`. + +### query.graph + +```ts +const { data: fulfillments } = await query.graph({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) + +// fulfillments.order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillments } = useQueryGraphStep({ + entity: "fulfillment", + fields: [ + "order.*", + ], +}) + +// fulfillments.order +``` + +### Manage with Link + +To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_id: "ful_123", + }, +}) +``` + +*** + +## Pricing Module + +The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. + +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. + +![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) + +### Retrieve with Query + +To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: + +### query.graph + +```ts +const { data: shippingOptions } = await query.graph({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingOptions } = useQueryGraphStep({ + entity: "shipping_option", + fields: [ + "price_set_link.*", + ], +}) + +// shippingOptions[0].price_set_link?.price_set_id +``` + +### Manage with Link + +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +*** + +## Product Module + +Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. + +This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). + +### Retrieve with Query + +To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: + +### query.graph + +```ts +const { data: shippingProfiles } = await query.graph({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: [ + "products.*", + ], +}) + +// shippingProfiles[0].products +``` + +### Manage with Link + +To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + product_id: "prod_123", + }, + [Modules.FULFILLMENT]: { + shipping_profile_id: "sp_123", + }, +}) +``` + +*** + +## Stock Location Module + +The Stock Location Module provides features to manage stock locations in a store. + +Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) + +Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. + +![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) + +### Retrieve with Query + +To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: + +To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. + +### query.graph + +```ts +const { data: fulfillmentSets } = await query.graph({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: fulfillmentSets } = useQueryGraphStep({ + entity: "fulfillment_set", + fields: [ + "location.*", + ], +}) + +// fulfillmentSets[0].location +``` + +### Manage with Link + +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + + +# Customer Accounts + +In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. + +## `has_account` Property + +The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. + +When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. + +When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. + +*** + +## Email Uniqueness + +The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. + +So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. + + +# Links between Customer Module and Other Modules + +This document showcases the module links defined between the Customer Module and other Commerce Modules. + +## Summary + +The Customer Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Customer|Read-only - has one|Learn more| + +*** + +## Payment Module + +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. + +This link is available starting from Medusa `v2.5.0`. + +### Retrieve with Query + +To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: customers } = await query.graph({ + entity: "customer", + fields: [ + "account_holder_link.account_holder.*", + ], +}) + +// customers[0].account_holder_link?.[0]?.account_holder +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: customers } = useQueryGraphStep({ + entity: "customer", + fields: [ + "account_holder_link.account_holder.*", + ], +}) + +// customers[0].account_holder_link?.[0]?.account_holder +``` + +### Manage with Link + +To manage the account holders of a customer, 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.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +*** + +## Cart Module + +Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. + +### Retrieve with Query + +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: carts } = await query.graph({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "customer.*", + ], +}) + +// carts.customer +``` + +*** + +## Order Module + +Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. + +### Retrieve with Query + +To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: orders } = await query.graph({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "customer.*", + ], +}) + +// orders.customer +``` + + # Inventory Concepts In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. @@ -23439,6 +24346,67 @@ A reservation item, represented by the [ReservationItem](https://docs.medusajs.c The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. +# Inventory Module in Medusa Flows + +This document explains how the Inventory Module is used within the Medusa application's flows. + +## Product Variant Creation + +When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. + +This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) + +*** + +## Add to Cart + +When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. + +This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) + +*** + +## Order Placed + +When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. + +This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) + +*** + +## Order Fulfillment + +When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: + +- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. +- Resets the `reserved_quantity` to `0`. +- Deletes the associated reservation item. + +This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) + +*** + +## Order Return + +When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. + +This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) + +![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) + +### Dismissed Returned Items + +If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. + + # Inventory Kits In this guide, you'll learn how inventory kits can be used in the Medusa application to support use cases like multi-part products, bundled products, and shared inventory across products. @@ -23829,67 +24797,6 @@ The bundled product has the same inventory items as those of the products part o 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). -# 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. @@ -24031,710 +24938,6 @@ const { data: inventoryLevels } = useQueryGraphStep({ ``` -# Fulfillment Concepts - -In this document, you’ll learn about some basic fulfillment concepts. - -## Fulfillment Set - -A fulfillment set is a general form or way of fulfillment. For example, shipping is a form of fulfillment, and pick-up is another form of fulfillment. Each of these can be created as fulfillment sets. - -A fulfillment set is represented by the [FulfillmentSet data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentSet/index.html.md). All other configurations, options, and management features are related to a fulfillment set, in one way or another. - -```ts -const fulfillmentSets = await fulfillmentModuleService.createFulfillmentSets( - [ - { - name: "Shipping", - type: "shipping", - }, - { - name: "Pick-up", - type: "pick-up", - }, - ] -) -``` - -*** - -## Service Zone - -A service zone is a collection of geographical zones or areas. It’s used to restrict available shipping options to a defined set of locations. - -A service zone is represented by the [ServiceZone data model](https://docs.medusajs.com/references/fulfillment/models/ServiceZone/index.html.md). It’s associated with a fulfillment set, as each service zone is specific to a form of fulfillment. For example, if a customer chooses to pick up items, you can restrict the available shipping options based on their location. - -![A diagram showcasing the relation between fulfillment sets, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712329770/Medusa%20Resources/service-zone_awmvfs.jpg) - -A service zone can have multiple geographical zones, each represented by the [GeoZone data model](https://docs.medusajs.com/references/fulfillment/models/GeoZone/index.html.md). It holds location-related details to narrow down supported areas, such as country, city, or province code. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -*** - -## Shipping Profile - -A shipping profile defines a type of items that are shipped in a similar manner. For example, a `default` shipping profile is used for all item types, but the `digital` shipping profile is used for digital items that aren’t shipped and delivered conventionally. - -A shipping profile is represented by the [ShippingProfile data model](https://docs.medusajs.com/references/fulfillment/models/ShippingProfile/index.html.md). It only defines the profile’s details, but it’s associated with the shipping options available for the item type. - - -# Fulfillment Module Provider - -In this guide, you’ll learn about the Fulfillment Module Provider and how it's used. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/locations-and-shipping/locations#manage-fulfillment-providers/index.html.md) to learn how to add a fulfillment provider to a location using the dashboard. - -## What is 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). - -![Diagram showcasing the communication between Medusa, the Fulfillment Module Provider, and the third-party fulfillment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794800/Medusa%20Resources/fulfillment-provider-service_ljsqpq.jpg) - -*** - -## Default Fulfillment Provider - -Medusa provides a Manual Fulfillment Provider that acts as a placeholder fulfillment provider. It doesn't process fulfillment and delegates that to the merchant. - -This provider is installed by default in your application and you can use it to fulfill items manually. - -The identifier of the manual fulfillment provider is `fp_manual_manual`. - -*** - -## How to Create a Custom Fulfillment Provider? - -A Fulfillment Module Provider is a module whose service implements the `IFulfillmentProvider` imported from `@medusajs/framework/types`. - -The module can have multiple fulfillment provider services, where each are registered as separate fulfillment providers. - -Refer to the [Create Fulfillment Module Provider](https://docs.medusajs.com/references/fulfillment/provider/index.html.md) guide to learn how to create a Fulfillment Module Provider. - -{/* TODO add link to user guide */} - -After you create a fulfillment provider, you can choose it as the default Fulfillment Module Provider for a stock location in the Medusa Admin dashboard. - -*** - -## How are Fulfillment Providers Registered? - -### Configure Fulfillment Module's Providers - -The Fulfillment Module accepts a `providers` option that allows you to configure the providers registered in your application. - -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/module-options/index.html.md) guide. - -### Registration on Application Start - -When the Medusa application starts, it registers the Fulfillment Module Providers defined in the `providers` option of the Fulfillment Module. - -For each Fulfillment Module Provider, the Medusa application finds all fulfillment provider services defined in them to register. - -### FulfillmentProvider Data Model - -A registered fulfillment provider is represented by the [FulfillmentProvider data model](https://docs.medusajs.com/references/fulfillment/models/FulfillmentProvider/index.html.md) in the Medusa application. - -This data model is used to reference a service in the Fulfillment Module Provider and determine whether it's installed in the application. - -![Diagram showcasing the FulfillmentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746794803/Medusa%20Resources/fulfillment-provider-model_wo2ato.jpg) - -The `FulfillmentProvider` data model has the following properties: - -- `id`: The unique identifier of the fulfillment provider. The ID's format is `fp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Fulfillment Module Provider's service. - - `id` is the value of the `id` property of the Fulfillment Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the fulfillment provider is enabled. - -### How to Remove a Fulfillment Provider? - -You can remove a registered fulfillment provider from the Medusa application by removing it from the `providers` option in the Fulfillment Module's configuration. - -Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `FulfillmentProvider`'s record to `false`. This allows you to re-enable the fulfillment provider later if needed by adding it back to the `providers` option. - - -# Item Fulfillment - -In this document, you’ll learn about the concepts of item fulfillment. - -## Fulfillment Data Model - -A fulfillment is the shipping and delivery of one or more items to the customer. It’s represented by the [Fulfillment data model](https://docs.medusajs.com/references/fulfillment/models/Fulfillment/index.html.md). - -*** - -## Fulfillment Processing by a Fulfillment Provider - -A fulfillment is associated with a fulfillment provider that handles all its processing, such as creating a shipment for the fulfillment’s items. - -The fulfillment is also associated with a shipping option of that provider, which determines how the item is shipped. - -![A diagram showcasing the relation between a fulfillment, fulfillment provider, and shipping option](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331947/Medusa%20Resources/fulfillment-shipping-option_jk9ndp.jpg) - -*** - -## data Property - -The `Fulfillment` data model has a `data` property that holds any necessary data for the third-party fulfillment provider to process the fulfillment. - -For example, the `data` property can hold the ID of the fulfillment in the third-party provider. The associated fulfillment provider then uses it whenever it retrieves the fulfillment’s details. - -*** - -## Fulfillment Items - -A fulfillment is used to fulfill one or more items. Each item is represented by the `FulfillmentItem` data model. - -The fulfillment item holds details relevant to fulfilling the item, such as barcode, SKU, and quantity to fulfill. - -![A diagram showcasing the relation between fulfillment and fulfillment items.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712332114/Medusa%20Resources/fulfillment-item_etzxb0.jpg) - -*** - -## Fulfillment Label - -Once a shipment is created for the fulfillment, you can store its tracking number, URL, or other related details as a label, represented by the `FulfillmentLabel` data model. - -*** - -## Fulfillment Status - -The `Fulfillment` data model has three properties to keep track of the current status of the fulfillment: - -- `packed_at`: The date the fulfillment was packed. If set, then the fulfillment has been packed. -- `shipped_at`: The date the fulfillment was shipped. If set, then the fulfillment has been shipped. -- `delivered_at`: The date the fulfillment was delivered. If set, then the fulfillment has been delivered. - - -# Links between Fulfillment Module and Other Modules - -This document showcases the module links defined between the Fulfillment Module and other Commerce Modules. - -## Summary - -The Fulfillment Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Order|Fulfillment|Stored - one-to-many|Learn more| -|Return|Fulfillment|Stored - one-to-many|Learn more| -|PriceSet|ShippingOption|Stored - many-to-one|Learn more| -|Product|ShippingProfile|Stored - many-to-one|Learn more| -|StockLocation|FulfillmentProvider|Stored - one-to-many|Learn more| -|StockLocation|FulfillmentSet|Stored - one-to-many|Learn more| - -*** - -## Order Module - -The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management functionalities. - -Medusa defines a link between the `Fulfillment` and `Order` data models. A fulfillment is created for an orders' items. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716549903/Medusa%20Resources/order-fulfillment_h0vlps.jpg) - -A fulfillment is also created for a return's items. So, Medusa defines a link between the `Fulfillment` and `Return` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399052/Medusa%20Resources/Social_Media_Graphics_2024_Order_Return_vetimk.jpg) - -### Retrieve with Query - -To retrieve the order of a fulfillment with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -To retrieve the return, pass `return.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillments } = await query.graph({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillments } = useQueryGraphStep({ - entity: "fulfillment", - fields: [ - "order.*", - ], -}) - -// fulfillments.order -``` - -### Manage with Link - -To manage the order of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_id: "ful_123", - }, -}) -``` - -*** - -## Pricing Module - -The Pricing Module provides features to store, manage, and retrieve the best prices in a specified context. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the price set of a shipping option with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `price_set.*` in `fields`: - -### query.graph - -```ts -const { data: shippingOptions } = await query.graph({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingOptions } = useQueryGraphStep({ - entity: "shipping_option", - fields: [ - "price_set_link.*", - ], -}) - -// shippingOptions[0].price_set_link?.price_set_id -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -Medusa defines a link between the `ShippingProfile` data model and the `Product` data model of the Product Module. Each product must belong to a shipping profile. - -This link is introduced in [Medusa v2.5.0](https://github.com/medusajs/medusa/releases/tag/v2.5.0). - -### Retrieve with Query - -To retrieve the products of a shipping profile with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `products.*` in `fields`: - -### query.graph - -```ts -const { data: shippingProfiles } = await query.graph({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: [ - "products.*", - ], -}) - -// shippingProfiles[0].products -``` - -### Manage with Link - -To manage the shipping profile of a product, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - product_id: "prod_123", - }, - [Modules.FULFILLMENT]: { - shipping_profile_id: "sp_123", - }, -}) -``` - -*** - -## Stock Location Module - -The Stock Location Module provides features to manage stock locations in a store. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. A fulfillment set can be conditioned to a specific stock location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the stock location of a fulfillment set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `location.*` in `fields`: - -To retrieve the stock location of a fulfillment provider, pass `locations.*` in `fields`. - -### query.graph - -```ts -const { data: fulfillmentSets } = await query.graph({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: fulfillmentSets } = useQueryGraphStep({ - entity: "fulfillment_set", - fields: [ - "location.*", - ], -}) - -// fulfillmentSets[0].location -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: "fset_123", - }, -}) -``` - - -# Fulfillment Module Options - -In this document, you'll learn about the options of the Fulfillment Module. - -## providers - -The `providers` option is an array of fulfillment module providers. - -When the Medusa application starts, these providers are registered and can be used to process fulfillments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/fulfillment", - options: { - providers: [ - { - resolve: `@medusajs/medusa/fulfillment-manual`, - id: "manual", - options: { - // provider options... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating either the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# Shipping Option - -In this document, you’ll learn about shipping options and their rules. - -## What’s a Shipping Option? - -A shipping option is a way of shipping an item. Each fulfillment provider provides a set of shipping options. For example, a provider may provide a shipping option for express shipping and another for standard shipping. - -When the customer places their order, they choose a shipping option to be used to fulfill their items. - -A shipping option is represented by the [ShippingOption data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOption/index.html.md). - -*** - -## Service Zone Restrictions - -A shipping option is restricted by a service zone, limiting the locations a shipping option be used in. - -For example, a fulfillment provider may have a shipping option that can be used in the United States, and another in Canada. - -![A diagram showcasing the relation between shipping options and service zones.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712330831/Medusa%20Resources/shipping-option-service-zone_pobh6k.jpg) - -Service zones can be more restrictive, such as restricting to certain cities or province codes. - -The province code is always in lower-case and in [ISO 3166-2 format](https://en.wikipedia.org/wiki/ISO_3166-2). - -![A diagram showcasing the relation between shipping options, service zones, and geo zones](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331186/Medusa%20Resources/shipping-option-service-zone-city_m5sxod.jpg) - -*** - -## Shipping Option Rules - -You can restrict shipping options by custom rules, such as the item’s weight or the customer’s group. - -You can also restrict a shipping option's price based on specific conditions. For example, you can make a shipping option's price free based on the cart's total. Learn more in the Pricing Module's [Price Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules#how-to-set-rules-on-a-price/index.html.md) guide. - -These rules are represented by the [ShippingOptionRule data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionRule/index.html.md). Its properties define the custom rule: - -- `attribute`: The name of a property or table that the rule applies to. For example, `customer_group`. -- `operator`: The operator used in the condition. For example: - - To allow multiple values, use the operator `in`, which validates that the provided values are in the rule’s values. - - To create a negation condition that considers `value` against the rule, use `nin`, which validates that the provided values aren’t in the rule’s values. - - Check out more operators in [this reference](https://docs.medusajs.com/references/fulfillment/types/fulfillment.RuleOperatorType/index.html.md). -- `value`: One or more values. - -![A diagram showcasing the relation between shipping option and shipping option rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331340/Medusa%20Resources/shipping-option-rule_oosopf.jpg) - -A shipping option can have multiple rules. For example, you can add rules to a shipping option so that it's available if the customer belongs to the VIP group and the total weight is less than 2000g. - -![A diagram showcasing how a shipping option can have multiple rules.](https://res.cloudinary.com/dza7lstvk/image/upload/v1712331462/Medusa%20Resources/shipping-option-rule-2_ylaqdb.jpg) - -*** - -## Shipping Profile and Types - -A shipping option belongs to a type. For example, a shipping option’s type may be `express`, while another `standard`. The type is represented by the [ShippingOptionType data model](https://docs.medusajs.com/references/fulfillment/models/ShippingOptionType/index.html.md). - -A shipping option also belongs to a shipping profile, as each shipping profile defines the type of items to be shipped in a similar manner. - -*** - -## data Property - -When fulfilling an item, you might use a third-party fulfillment provider that requires additional custom data to be passed along from the checkout or order-creation process. - -The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. - - -# Order Claim - -In this document, you’ll learn about order claims. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. - -## What is a Claim? - -When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. - -The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. - -*** - -## Claim Type - -The `Claim` data model has a `type` property whose value indicates the type of the claim: - -- `refund`: the items are returned, and the customer is refunded. -- `replace`: the items are returned, and the customer receives new items. - -*** - -## Old and Replacement Items - -When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. - -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). - -*** - -## Claim Shipping Methods - -A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). - -*** - -## Claim Refund - -If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. - -The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. - -*** - -## How Claims Impact an Order’s Version - -When a claim is confirmed, the order’s version is incremented. - - # Order Concepts In this document, you’ll learn about orders and related concepts @@ -24841,59 +25044,6 @@ Once the Order Edit is confirmed, any additional payment or refund required can This is determined by the comparison between the `OrderSummary` and the order's transactions, as mentioned in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/transactions#checking-outstanding-amount/index.html.md). -# Order Exchange - -In this document, you’ll learn about order exchanges. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. - -## What is an Exchange? - -An exchange is the replacement of an item that the customer ordered with another. - -A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. - -The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. - -*** - -## Returned and New Items - -When the exchange is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is created to handle receiving the items back from the customer. - -Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. - -*** - -## Exchange Shipping Methods - -An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). - -The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). - -*** - -## Exchange Payment - -The `Exchange` data model has a `difference_due` property that stores the outstanding amount. - -|Condition|Result| -|---|---|---| -|\`difference\_due \< 0\`|Merchant owes the customer a refund of the | -|\`difference\_due > 0\`|Merchant requires additional payment from the customer of the | -|\`difference\_due = 0\`|No payment processing is required.| - -Any payment or refund made is stored in the [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). - -*** - -## How Exchanges Impact an Order’s Version - -When an exchange is confirmed, the order’s version is incremented. - - # Links between Order Module and Other Modules This document showcases the module links defined between the Order Module and other Commerce Modules. @@ -25638,6 +25788,108 @@ The following diagram is a simplified showcase of how a subtotal is calculated f For example, if a line item's amount is `5000`, the tax rate is `10`, and `is_tax_inclusive` is enabled, the tax amount is 10% of `5000`, which is `500`. The item's unit price becomes `4500`. +# Transactions + +In this document, you’ll learn about an order’s transactions and its use. + +## What is a Transaction? + +A transaction represents any order payment process, such as capturing or refunding an amount. It’s represented by the [OrderTransaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md). + +The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. + +Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. + +*** + +## Checking Outstanding Amount + +The order’s total amounts are stored in the `OrderSummary`'s `totals` property, which is a JSON object holding the total details of the order. + +```json +{ + "totals": { + "total": 30, + "subtotal": 30, + // ... + } +} +``` + +To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: + +|Condition|Result| +|---|---|---| +|summary’s total - transaction amounts total = 0|There’s no outstanding amount.| +|summary’s total - transaction amounts total > 0|The customer owes additional payment to the merchant.| +|summary’s total - transaction amounts total \< 0|The merchant owes the customer a refund.| + +*** + +## Transaction Reference + +The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). + +The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: + +- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. +- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. + + +# Order Claim + +In this document, you’ll learn about order claims. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/claims/index.html.md) to learn how to manage an order's claims using the dashboard. + +## What is a Claim? + +When a customer receives a defective or incorrect item, the merchant can create a claim to refund or replace the item. + +The [OrderClaim data model](https://docs.medusajs.com/references/order/models/OrderClaim/index.html.md) represents a claim. + +*** + +## Claim Type + +The `Claim` data model has a `type` property whose value indicates the type of the claim: + +- `refund`: the items are returned, and the customer is refunded. +- `replace`: the items are returned, and the customer receives new items. + +*** + +## Old and Replacement Items + +When the claim is created, a return, represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md), is also created to handle receiving the old items from the customer. + +Learn more about returns in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +If the claim’s type is `replace`, replacement items are represented by the [ClaimItem data model](https://docs.medusajs.com/references/order/models/OrderClaimItem/index.html.md). + +*** + +## Claim Shipping Methods + +A claim uses shipping methods to send the replacement items to the customer. These methods are represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). + +The shipping methods for the returned items are associated with the claim's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). + +*** + +## Claim Refund + +If the claim’s type is `refund`, the amount to be refunded is stored in the `refund_amount` property. + +The [Transaction data model](https://docs.medusajs.com/references/order/models/OrderTransaction/index.html.md) represents the refunds made for the claim. + +*** + +## How Claims Impact an Order’s Version + +When a claim is confirmed, the order’s version is incremented. + + # Order Return In this document, you’ll learn about order returns. @@ -25699,1262 +25951,57 @@ The order’s version is incremented when: 2. A return is marked as received. -# Transactions +# Order Exchange -In this document, you’ll learn about an order’s transactions and its use. +In this document, you’ll learn about order exchanges. -## What is a Transaction? +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/exchanges/index.html.md) to learn how to manage an order's exchanges using the dashboard. -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). +## What is an Exchange? -The transaction’s main purpose is to ensure a correct balance between paid and outstanding amounts. +An exchange is the replacement of an item that the customer ordered with another. -Transactions are also associated with returns, claims, and exchanges if additional payment or refund is required. +A merchant creates the exchange, specifying the items to be replaced and the new items to be sent. + +The [OrderExchange data model](https://docs.medusajs.com/references/order/models/OrderExchange/index.html.md) represents an exchange. *** -## Checking Outstanding Amount +## Returned and New Items -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. +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. -```json -{ - "totals": { - "total": 30, - "subtotal": 30, - // ... - } -} -``` +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). -To check the outstanding amount of the order, its transaction amounts are summed. Then, the following conditions are checked: +The [OrderExchangeItem data model](https://docs.medusajs.com/references/order/models/OrderExchangeItem/index.html.md) represents the new items to be sent to the customer. + +*** + +## Exchange Shipping Methods + +An exchange has shipping methods used to send the new items to the customer. They’re represented by the [OrderShippingMethod data model](https://docs.medusajs.com/references/order/models/OrderShippingMethod/index.html.md). + +The shipping methods for the returned items are associated with the exchange's return, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return#return-shipping-methods/index.html.md). + +*** + +## Exchange Payment + +The `Exchange` data model has a `difference_due` property that stores the outstanding amount. |Condition|Result| |---|---|---| -|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.| +|\`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). *** -## Transaction Reference +## How Exchanges Impact an Order’s Version -The Order Module doesn’t provide payment processing functionalities, so it doesn’t store payments that can be processed. Payment functionalities are provided by the [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md). - -The `OrderTransaction` data model has two properties that determine which data model and record holds the actual payment’s details: - -- `reference`: indicates the table’s name in the database. For example, `payment` from the Payment Module. -- `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. - - -# Account Holders and Saved Payment Methods - -In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. - -Account holders are available starting from Medusa `v2.5.0`. - -## What's an Account Holder? - -An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. - -It holds fields retrieved from the third-party provider, such as: - -- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. -- `data`: Data returned by the payment provider when the account holder is created. - -A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. - -### Relation between Account Holder and Customer - -The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. - -This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. - -Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. - -*** - -## Save Payment Methods - -If a payment provider supports saving payment methods for a customer, they must implement the following methods: - -- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. -- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. -- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. -- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. - -Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). - -*** - -## Account Holder in Medusa Payment Flows - -In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. - -Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. - -This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). - - -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. - -## All Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| - -*** - -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# 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: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|PaymentCollection|Stored - one-to-one|Learn more| -|Customer|AccountHolder|Stored - many-to-many|Learn more| -|Order|PaymentCollection|Stored - one-to-many|Learn more| -|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| -|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| -|Region|PaymentProvider|Stored - many-to-many|Learn more| - -*** - -## 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[0].cart -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "cart.*", - ], -}) - -// paymentCollections[0].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", - }, -}) -``` - -*** - -## Customer Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. - -This link is available starting from Medusa `v2.5.0`. - -### Retrieve with Query - -To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: accountHolders } = await query.graph({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) - -// accountHolders[0].customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: accountHolders } = useQueryGraphStep({ - entity: "account_holder", - fields: [ - "customer.*", - ], -}) - -// accountHolders[0].customer -``` - -### Manage with Link - -To manage the account holders of a customer, 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.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_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[0].order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", - fields: [ - "order.*", - ], -}) - -// paymentCollections[0].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): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PAYMENT]: { - payment_collection_id: "paycol_123", - }, -}) -``` - -*** - -## Region Module - -You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models. - -![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) - -This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. - -### Retrieve with Query - -To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: - -### query.graph - -```ts -const { data: paymentProviders } = await query.graph({ - entity: "payment_provider", - fields: [ - "regions.*", - ], -}) - -// paymentProviders[0].regions -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", - fields: [ - "regions.*", - ], -}) - -// paymentProviders[0].regions -``` - -### Manage with Link - -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 - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.REGION]: { - region_id: "reg_123", - }, - [Modules.PAYMENT]: { - payment_provider_id: "pp_stripe_stripe", - }, -}) -``` - - -# Payment - -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. - -## What's a Payment? - -When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. - -A payment carries many of the data and relations of a payment session: - -- It belongs to the same payment collection. -- It’s associated with the same payment provider, which handles further payment processing. -- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. - -*** - -## Capture Payments - -When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. - -The payment can also be captured incrementally, each time a capture record is created for that amount. - -![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) - -*** - -## Refund Payments - -When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. - -A payment can be refunded multiple times, and each time a refund record is created. - -![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) - -*** - -## data Property - -Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider. - -The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment. - -Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider. - -If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. - - -# Payment Steps in Checkout Flow - -In this guide, you'll learn about Medusa's accept payment flow that's used in checkout. - -## Overview of the Payment Flow in Checkout - -The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout. - -This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers. - -The payment flow consists of the following steps: - -![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) - -- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart. - - This payment collection will hold all details related to the payment operations. -- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from. - - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region. -- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider. -- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves: - - Authorizing the payment session with the third-party payment provider. - - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion. - -*** - -## Implement Payment Checkout Step in Storefront - -If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step. - -If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step. - -*** - -{/* TODO add section on customizng the payment flow */} - -## Build a Custom Payment Flow - -You can also build a custom payment flow using workflows or the Payment Module's main service. - -Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more. - - -# 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) - - -# Accept Payment in Checkout Flow - -In this guide, you'll learn how to implement it using workflows or the Payment Module. - -## Why Implement the Payment Flow? - -Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide. - -You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application. - -This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows. - -You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application. - -*** - -## How to Implement the Accept Payment Flow? - -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). - -It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. - -### 1. Create a Payment Collection - -A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. - -In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for. - -For example: - -### Using Workflow - -```ts -import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" - -// ... - -await createPaymentCollectionForCartWorkflow(container) - .run({ - input: { - cart_id: "cart_123", - }, - }) -``` - -### Using Service - -```ts -const paymentCollection = - await paymentModuleService.createPaymentCollections({ - currency_code: "usd", - amount: 5000, - }) -``` - -### 2. Show Payment Providers - -Next, you'll show the customer the available payment providers to choose from. - -In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region. - -### Using Query - -```ts -const query = container.resolve("query") - -const { data: regionPaymentProviders } = await query.graph({ - entryPoint: "region_payment_provider", - variables: { - filters: { - region_id: "reg_123", - }, - }, - fields: ["payment_providers.*"], -}) - -const paymentProviders = regionPaymentProviders.map( - (relation) => relation.payment_providers -) -``` - -### Using Service - -```ts -const paymentProviders = await paymentModuleService.listPaymentProviders() -``` - -### 3. Create Payment Sessions - -The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. - -So, once the customer selects a payment provider, create a payment session for the selected payment provider. - -This will also initialize the payment session in the third-party payment provider. - -For example: - -### Using Workflow - -```ts -import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" - -// ... - -const { result: paymentSesion } = await createPaymentSessionsWorkflow(container) - .run({ - input: { - payment_collection_id: "paycol_123", - provider_id: "pp_stripe_stripe", - }, - }) -``` - -### Using Service - -```ts -const paymentSession = - await paymentModuleService.createPaymentSession( - paymentCollection.id, - { - provider_id: "pp_stripe_stripe", - currency_code: "usd", - amount: 5000, - data: { - // any necessary data for the - // payment provider - }, - } - ) -``` - -### 4. Authorize Payment Session - -Once the customer places the order, you need to authorize the payment session with the third-party payment provider. - -For example: - -### Using Step - -```ts -import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" - -// ... - -authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) -``` - -### Using Service - -```ts -const payment = authorizePaymentSessionStep({ - id: "payses_123", - context: {}, -}) -``` - -When the payment authorization is successful, a payment is created and returned. - -#### Handling Additional Action - -If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. - -If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. - -In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. - -For example: - -```ts -try { - const payment = - await paymentModuleService.authorizePaymentSession( - paymentSession.id, - {} - ) -} catch (e) { - // retrieve the payment session again - const updatedPaymentSession = ( - await paymentModuleService.listPaymentSessions({ - id: [paymentSession.id], - }) - )[0] - - if (updatedPaymentSession.status === "requires_more") { - // TODO perform required action - // TODO authorize payment again. - } -} -``` - -### 5. Payment Flow Complete - -The payment flow is complete once the payment session is authorized and the payment is created. - -You can then: - -- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application. -- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). -- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). - -Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. - - -# Payment Module Provider - -In this guide, you’ll learn about the Payment Module Provider and how it's used. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. - -*** - -## What is a Payment Module Provider? - -The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md). - -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. - -![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg) - -### List of Payment Module Providers - -- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) - -### Default 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. - -The identifier of the system payment provider is `pp_system`. - -*** - -## How to Create a Custom Payment Provider? - -A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. - -The module can have multiple payment provider services, where each is registered as a separate payment provider. - -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. - -After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md). - -*** - -## How are Payment Providers Registered? - -### Configure Payment Module's Providers - -The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application. - -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide. - -### Registration on Application Start - -When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module. - -For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register. - -### PaymentProvider Data Model - -A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application. - -![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg) - -This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application. - -The `PaymentProvider` data model has the following properties: - -- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Payment Module Provider's service. - - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the payment provider is enabled. - -### How to Remove a Payment Provider? - -If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record. - -Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option. - - -# Payment Session - -In this document, you’ll learn what a payment session is. - -## What's a Payment Session? - -A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. - -A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. - -![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) - -*** - -## data Property - -Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider. - -The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized. - -Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider. - -If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. - -### data Property in the Storefront - -This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront. - -For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider. - -*** - -## Payment Session Status - -The `status` property of a payment session indicates its current status. Its value can be: - -- `pending`: The payment session is awaiting authorization. -- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. -- `authorized`: The payment session is authorized. -- `error`: An error occurred while authorizing the payment. -- `canceled`: The authorization of the payment session has been canceled. - - -# Payment Webhook Events - -In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module. - -## What's a Payment Webhook Event? - -A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status. - -This is useful in many cases such as: - -- When a payment is processed (authorized or captured) asynchronously. -- When a payment is managed on the third-party payment provider's side. -- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application. - -So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions. - -*** - -## How to Handle Payment Webhook Events - -### Webhook Listener API Route - -The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where: - -- `[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`. - -You can use this webhook listener when configuring webhook events in your third-party payment provider. - -### getWebhookActionAndData Method - -The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider. - -Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events. - -![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) - -If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions: - -View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference. - -- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`. -- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`. -- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart. - - -# Links between Currency Module and Other Modules - -This document showcases the module links defined between the Currency Module and other Commerce Modules. - -## Summary - -The Currency Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|StoreCurrency|Currency|Read-only - has one|Learn more| - -*** - -## Store Module - -The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. - -Instead, Medusa defines a read-only link between the [Store Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/store/index.html.md)'s `StoreCurrency` data model and the Currency Module's `Currency` data model. Because the link is read-only from the `Store`'s side, you can only retrieve the details of a store's supported currencies, and not the other way around. - -### Retrieve with Query - -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: - -### query.graph - -```ts -const { data: stores } = await query.graph({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies[0].currency -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: [ - "supported_currencies.currency.*", - ], -}) - -// stores[0].supported_currencies[0].currency -``` - - -# Customer Accounts - -In this document, you’ll learn how registered and unregistered accounts are distinguished in the Medusa application. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/customers/index.html.md) to learn how to manage customers using the dashboard. - -## `has_account` Property - -The [Customer data model](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) has a `has_account` property, which is a boolean that indicates whether a customer is registered. - -When a guest customer places an order, a new `Customer` record is created with `has_account` set to `false`. - -When this or another guest customer registers an account with the same email, a new `Customer` record is created with `has_account` set to `true`. - -*** - -## Email Uniqueness - -The above behavior means that two `Customer` records may exist with the same email. However, the main difference is the `has_account` property's value. - -So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. - - -# Links between Customer Module and Other Modules - -This document showcases the module links defined between the Customer Module and other Commerce Modules. - -## Summary - -The Customer Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Customer|AccountHolder|Stored - many-to-many|Learn more| -|Cart|Customer|Read-only - has one|Learn more| -|Order|Customer|Read-only - has one|Learn more| - -*** - -## Payment Module - -Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. - -This link is available starting from Medusa `v2.5.0`. - -### Retrieve with Query - -To retrieve the account holder associated with a customer with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: customers } = await query.graph({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) - -// customers[0].account_holder_link?.[0]?.account_holder -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: customers } = useQueryGraphStep({ - entity: "customer", - fields: [ - "account_holder_link.account_holder.*", - ], -}) - -// customers[0].account_holder_link?.[0]?.account_holder -``` - -### Manage with Link - -To manage the account holders of a customer, 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.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CUSTOMER]: { - customer_id: "cus_123", - }, - [Modules.PAYMENT]: { - account_holder_id: "acchld_123", - }, -}) -``` - -*** - -## Cart Module - -Medusa defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `Cart` data model and the `Customer` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the customer of a cart, and not the other way around. - -### Retrieve with Query - -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: carts } = await query.graph({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts.customer -``` - -*** - -## Order Module - -Medusa defines a read-only link between the [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md)'s `Order` data model and the `Customer` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the customer of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the customer of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "customer.*", - ], -}) - -// orders.customer -``` +When an exchange is confirmed, the order’s version is incremented. # Pricing Concepts @@ -26980,190 +26027,6 @@ A price list has optional `start_date` and `end_date` properties that indicate t Its associated prices are represented by the `Price` data model. -# Links between Pricing Module and Other Modules - -This document showcases the module links defined between the Pricing Module and other Commerce Modules. - -## Summary - -The Pricing Module has the following links to other modules: - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|ShippingOption|PriceSet|Stored - one-to-one|Learn more| -|ProductVariant|PriceSet|Stored - one-to-one|Learn more| - -*** - -## Fulfillment Module - -The Fulfillment Module provides fulfillment-related functionalities, including shipping options that the customer chooses from when they place their order. However, it doesn't provide pricing-related functionalities for these options. - -Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. - -![A diagram showcasing an example of how data models from the Pricing and Fulfillment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716561747/Medusa%20Resources/pricing-fulfillment_spywwa.jpg) - -### Retrieve with Query - -To retrieve the shipping option of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `shipping_option.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "shipping_option.*", - ], -}) - -// priceSets[0].shipping_option -``` - -### Manage with Link - -To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.FULFILLMENT]: { - shipping_option_id: "so_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -*** - -## Product Module - -The Product Module doesn't store or manage the prices of product variants. - -Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. - -![A diagram showcasing an example of how data models from the Pricing and Product Module are linked. The PriceSet is linked to the ProductVariant of the Product Module.](https://res.cloudinary.com/dza7lstvk/image/upload/v1709651039/Medusa%20Resources/pricing-product_m4xaut.jpg) - -So, when you want to add prices for a product variant, you create a price set and add the prices to it. - -You can then benefit from adding rules to prices or using the `calculatePrices` method to retrieve the price of a product variant within a specified context. - -### Retrieve with Query - -To retrieve the variant of a price set with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -### query.graph - -```ts -const { data: priceSets } = await query.graph({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: priceSets } = useQueryGraphStep({ - entity: "price_set", - fields: [ - "variant.*", - ], -}) - -// priceSets[0].variant -``` - -### Manage with Link - -To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.PRODUCT]: { - variant_id: "variant_123", - }, - [Modules.PRICING]: { - price_set_id: "pset_123", - }, -}) -``` - - # Prices Calculation In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service. @@ -27659,72 +26522,188 @@ In this example, the price is only applied if a cart's customer belongs to the c These same rules apply to product variant prices as well, or any other resource that has a price. -# Tax-Inclusive Pricing +# Links between Pricing Module and Other Modules -In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. +This document showcases the module links defined between the Pricing Module and other Commerce Modules. -## What is Tax-Inclusive Pricing? +## Summary -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. +The Pricing Module has the following links to other modules: -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. +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|ShippingOption|PriceSet|Stored - one-to-one|Learn more| +|ProductVariant|PriceSet|Stored - one-to-one|Learn more| *** -## How is Tax-Inclusive Pricing Set? +## Fulfillment Module -The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: +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. -- `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`. +Medusa defines a link between the `PriceSet` and `ShippingOption` data models. A shipping option's price is stored as a price set. -Only `region_id` and `currency_code` are supported as an `attribute` at the moment. +![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) -The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. +### Retrieve with Query -For example: +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`: -```json -{ - "attribute": "currency_code", - "value": "USD", - "is_tax_inclusive": true, -} +### query.graph + +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) + +// priceSets[0].shipping_option ``` -In this example, tax-inclusivity is enabled for the `USD` currency code. +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "shipping_option.*", + ], +}) + +// priceSets[0].shipping_option +``` + +### Manage with Link + +To manage the price set of a shipping option, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.FULFILLMENT]: { + shipping_option_id: "so_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` *** -## Tax-Inclusive Pricing in Price Calculation +## Product Module -### Tax Context +The Product Module doesn't store or manage the prices of product variants. -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. +Medusa defines a link between the `ProductVariant` and the `PriceSet`. A product variant’s prices are stored as prices belonging to a price set. -To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. +![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) -### Returned Tax Properties +So, when you want to add prices for a product variant, you create a price set and add the prices to it. -The `calculatePrices` method returns two properties related to tax-inclusivity: +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. -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). +### Retrieve with Query -- `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. +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`: -A price is considered tax-inclusive if: +### query.graph -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. +```ts +const { data: priceSets } = await query.graph({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) -### Tax Context Precedence +// priceSets[0].variant +``` -A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: +### useQueryGraphStep -- 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 +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: priceSets } = useQueryGraphStep({ + entity: "price_set", + fields: [ + "variant.*", + ], +}) + +// priceSets[0].variant +``` + +### Manage with Link + +To manage the price set of a variant, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.PRODUCT]: { + variant_id: "variant_123", + }, + [Modules.PRICING]: { + price_set_id: "pset_123", + }, +}) +``` # Links between Product Module and Other Modules @@ -28291,6 +27270,1027 @@ The following guides provide more details on inventory management in the Medusa - [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). +# Account Holders and Saved Payment Methods + +In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers. + +Account holders are available starting from Medusa `v2.5.0`. + +## What's an Account Holder? + +An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the `AccountHolder` data model. + +It holds fields retrieved from the third-party provider, such as: + +- `external_id`: The ID of the equivalent customer or account holder in the third-party provider. +- `data`: Data returned by the payment provider when the account holder is created. + +A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider. + +### Relation between Account Holder and Customer + +The Medusa application creates a link between the [Customer](https://docs.medusajs.com/references/customer/models/Customer/index.html.md) data model of the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md) and the `AccountHolder` data model of the Payment Module. + +This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers. + +Learn more about this link in the [Link to Other Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/links-to-other-modules/index.html.md) guide. + +*** + +## Save Payment Methods + +If a payment provider supports saving payment methods for a customer, they must implement the following methods: + +- `createAccountHolder`: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like `external_id` and `data` in the created `AccountHolder` record. +- `deleteAccountHolder`: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa. +- `savePaymentMethod`: Saves a payment method for an account holder in the payment provider. +- `listPaymentMethods`: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront. + +Learn more about implementing these methods in the [Create Payment Provider guide](https://docs.medusajs.com/references/payment/provider/index.html.md). + +*** + +## Account Holder in Medusa Payment Flows + +In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer. + +Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa. + +This flow is only supported if the chosen payment provider has implemented the necessary [save payment methods](#save-payment-methods). + + +# Payment Module Options + +In this document, you'll learn about the options of the Payment Module. + +## All Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| +|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| +|\`providers\`|An array of payment providers to install and register. Learn more |No|-| + +*** + +## providers Option + +The `providers` option is an array of payment module providers. + +When the Medusa application starts, these providers are registered and can be used to process payments. + +For example: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +The `providers` option is an array of objects that accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# 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: + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|Customer|AccountHolder|Stored - many-to-many|Learn more| +|Order|PaymentCollection|Stored - one-to-many|Learn more| +|OrderClaim|PaymentCollection|Stored - one-to-many|Learn more| +|OrderExchange|PaymentCollection|Stored - one-to-many|Learn more| +|Region|PaymentProvider|Stored - many-to-many|Learn more| + +*** + +## 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[0].cart +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "cart.*", + ], +}) + +// paymentCollections[0].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", + }, +}) +``` + +*** + +## Customer Module + +Medusa defines a link between the `Customer` and `AccountHolder` data models, allowing payment providers to save payment methods for a customer, if the payment provider supports it. + +This link is available starting from Medusa `v2.5.0`. + +### Retrieve with Query + +To retrieve the customer associated with an account holder with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: + +### query.graph + +```ts +const { data: accountHolders } = await query.graph({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) + +// accountHolders[0].customer +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: accountHolders } = useQueryGraphStep({ + entity: "account_holder", + fields: [ + "customer.*", + ], +}) + +// accountHolders[0].customer +``` + +### Manage with Link + +To manage the account holders of a customer, 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.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CUSTOMER]: { + customer_id: "cus_123", + }, + [Modules.PAYMENT]: { + account_holder_id: "acchld_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[0].order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: paymentCollections } = useQueryGraphStep({ + entity: "payment_collection", + fields: [ + "order.*", + ], +}) + +// paymentCollections[0].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): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PAYMENT]: { + payment_collection_id: "paycol_123", + }, +}) +``` + +*** + +## Region Module + +You can specify for each region which payment providers are available. The Medusa application defines a link between the `PaymentProvider` and the `Region` data models. + +![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) + +This increases the flexibility of your store. For example, you only show during checkout the payment providers associated with the cart's region. + +### Retrieve with Query + +To retrieve the regions of a payment provider with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `regions.*` in `fields`: + +### query.graph + +```ts +const { data: paymentProviders } = await query.graph({ + entity: "payment_provider", + fields: [ + "regions.*", + ], +}) + +// paymentProviders[0].regions +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: paymentProviders } = useQueryGraphStep({ + entity: "payment_provider", + fields: [ + "regions.*", + ], +}) + +// paymentProviders[0].regions +``` + +### Manage with Link + +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 + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.REGION]: { + region_id: "reg_123", + }, + [Modules.PAYMENT]: { + payment_provider_id: "pp_stripe_stripe", + }, +}) +``` + + +# Payment + +In this document, you’ll learn what a payment is and how it's created, captured, and refunded. + +## What's a Payment? + +When a payment session is authorized, a payment, represented by the [Payment data model](https://docs.medusajs.com/references/payment/models/Payment/index.html.md), is created. This payment can later be captured or refunded. + +A payment carries many of the data and relations of a payment session: + +- It belongs to the same payment collection. +- It’s associated with the same payment provider, which handles further payment processing. +- It stores the payment session’s `data` property in its `data` property, as it’s still useful for the payment provider’s processing. + +*** + +## Capture Payments + +When a payment is captured, a capture, represented by the [Capture data model](https://docs.medusajs.com/references/payment/models/Capture/index.html.md), is created. It holds details related to the capture, such as the amount, the capture date, and more. + +The payment can also be captured incrementally, each time a capture record is created for that amount. + +![A diagram showcasing how a payment's multiple captures are stored](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565445/Medusa%20Resources/payment-capture_f5fve1.jpg) + +*** + +## Refund Payments + +When a payment is refunded, a refund, represented by the [Refund data model](https://docs.medusajs.com/references/payment/models/Refund/index.html.md), is created. It holds details related to the refund, such as the amount, refund date, and more. + +A payment can be refunded multiple times, and each time a refund record is created. + +![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) + +*** + +## data Property + +Payment providers may need additional data to process the payment later. For example, the ID of the associated payment in the third-party provider. + +The `Payment` data model has a `data` property used to store that data. The first time it's set is when the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) authorizes the payment. + +Then, the `data` property is passed to the Medusa payment provider when the payment is captured or refunded, allowing the payment provider to utilize the data to process the payment with the third-party provider. + +If you're building a custom payment provider, learn more about authorizing and capturing the payments and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. + + +# Tax-Inclusive Pricing + +In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. + +## What is Tax-Inclusive Pricing? + +A tax-inclusive price is a price of a resource that includes taxes. Medusa calculates the tax amount from the price rather than adds the amount to it. + +For example, if a product’s price is $50, the tax rate is 2%, and tax-inclusive pricing is enabled, then the product's price is $49, and the applied tax amount is $1. + +*** + +## How is Tax-Inclusive Pricing Set? + +The [PricePreference data model](https://docs.medusajs.com/references/pricing/models/PricePreference/index.html.md) holds the tax-inclusive setting for a context. It has two properties that indicate the context: + +- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`. +- `value`: The attribute’s value. For example, `reg_123` or `usd`. + +Only `region_id` and `currency_code` are supported as an `attribute` at the moment. + +The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context. + +For example: + +```json +{ + "attribute": "currency_code", + "value": "USD", + "is_tax_inclusive": true, +} +``` + +In this example, tax-inclusivity is enabled for the `USD` currency code. + +*** + +## Tax-Inclusive Pricing in Price Calculation + +### Tax Context + +As mentioned in the [Price Calculation documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), The `calculatePrices` method accepts as a parameter a calculation context. + +To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context. + +### Returned Tax Properties + +The `calculatePrices` method returns two properties related to tax-inclusivity: + +Learn more about the returned properties in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). + +- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive. +- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive. + +A price is considered tax-inclusive if: + +1. It belongs to the region or currency code specified in the calculation context; +2. and the region or currency code has a price preference with `is_tax_inclusive` enabled. + +### Tax Context Precedence + +A region’s price preference’s `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive if: + +- both the `region_id` and `currency_code` are provided in the calculation context; +- the selected price belongs to the region; +- and the region has a price preference + + +# 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 Steps in Checkout Flow + +In this guide, you'll learn about Medusa's accept payment flow that's used in checkout. + +## Overview of the Payment Flow in Checkout + +The Medusa application has a built-in payment flow that allows you to accept payments from customers, typically during checkout. + +This flow is designed to be flexible and extensible, allowing you to integrate with various payment providers. + +The payment flow consists of the following steps: + +![A diagram showcasing the payment flow's steps](https://res.cloudinary.com/dza7lstvk/image/upload/v1711566781/Medusa%20Resources/payment-flow_jblrvw.jpg) + +- [Create Payment Collection](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollections): Create a payment collection associated with a cart. + - This payment collection will hold all details related to the payment operations. +- [Show Payment Providers](https://docs.medusajs.com/api/store#payment-providers_getpaymentproviders): Show the customer the available payment providers to choose from. + - You can integrate any [payment provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md), and you can enable them per region. +- [Create and Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions): Create a payment session for the selected payment provider in the Medusa application, and initialize the session in the third-party payment provider. +- [Complete Cart](https://docs.medusajs.com/api/store#carts_postcartsidcomplete): Once the customer places the order, complete the cart, which involves: + - Authorizing the payment session with the third-party payment provider. + - If the third-party payment provider requires performing additional actions, show them to the customer, then retry cart completion. + +*** + +## Implement Payment Checkout Step in Storefront + +If you're using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), the checkout flow is already implemented with the payment step. + +If you're building a custom storefront, or you want to customize the checkout flow, you can follow the [Checkout in Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/checkout/index.html.md) guide to learn how to build the checkout flow in the storefront, including the payment step. + +*** + +{/* TODO add section on customizng the payment flow */} + +## Build a Custom Payment Flow + +You can also build a custom payment flow using workflows or the Payment Module's main service. + +Refer to the [Accept Payment Flow](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-flow/index.html.md) guide to learn more. + + +# Accept Payment in Checkout Flow + +In this guide, you'll learn how to implement it using workflows or the Payment Module. + +## Why Implement the Payment Flow? + +Medusa already provides a built-in payment flow that allows you to accept payments from customers, which you can learn about in the [Accept Payment Flow in Checkout](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-checkout-flow/index.html.md) guide. + +You may need to implement a custom payment flow if you have a different use case, or you're using the Payment Module separately from the Medusa application. + +This guide will help you understand how to implement a payment flow using the Payment Module's main service or workflows. + +You can also follow this guide to get a general understanding of how the payment flow works in the Medusa application. + +*** + +## How to Implement the Accept Payment Flow? + +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). + +It's highly recommended to use Medusa's workflows to implement this flow. Use the Payment Module's main service for more complex cases. + +### 1. Create a Payment Collection + +A payment collection holds all details related to a resource’s payment operations. So, you start off by creating a payment collection. + +In the Medusa application, you associate the payment collection with a cart, which is the resource that the customer is trying to pay for. + +For example: + +### Using Workflow + +```ts +import { createPaymentCollectionForCartWorkflow } from "@medusajs/medusa/core-flows" + +// ... + +await createPaymentCollectionForCartWorkflow(container) + .run({ + input: { + cart_id: "cart_123", + }, + }) +``` + +### Using Service + +```ts +const paymentCollection = + await paymentModuleService.createPaymentCollections({ + currency_code: "usd", + amount: 5000, + }) +``` + +### 2. Show Payment Providers + +Next, you'll show the customer the available payment providers to choose from. + +In the Medusa application, you need to use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve the available payment providers in a region. + +### Using Query + +```ts +const query = container.resolve("query") + +const { data: regionPaymentProviders } = await query.graph({ + entryPoint: "region_payment_provider", + variables: { + filters: { + region_id: "reg_123", + }, + }, + fields: ["payment_providers.*"], +}) + +const paymentProviders = regionPaymentProviders.map( + (relation) => relation.payment_providers +) +``` + +### Using Service + +```ts +const paymentProviders = await paymentModuleService.listPaymentProviders() +``` + +### 3. Create Payment Sessions + +The payment collection has one or more payment sessions, each being a payment amount to be authorized by a payment provider. + +So, once the customer selects a payment provider, create a payment session for the selected payment provider. + +This will also initialize the payment session in the third-party payment provider. + +For example: + +### Using Workflow + +```ts +import { createPaymentSessionsWorkflow } from "@medusajs/medusa/core-flows" + +// ... + +const { result: paymentSesion } = await createPaymentSessionsWorkflow(container) + .run({ + input: { + payment_collection_id: "paycol_123", + provider_id: "pp_stripe_stripe", + }, + }) +``` + +### Using Service + +```ts +const paymentSession = + await paymentModuleService.createPaymentSession( + paymentCollection.id, + { + provider_id: "pp_stripe_stripe", + currency_code: "usd", + amount: 5000, + data: { + // any necessary data for the + // payment provider + }, + } + ) +``` + +### 4. Authorize Payment Session + +Once the customer places the order, you need to authorize the payment session with the third-party payment provider. + +For example: + +### Using Step + +```ts +import { authorizePaymentSessionStep } from "@medusajs/medusa/core-flows" + +// ... + +authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) +``` + +### Using Service + +```ts +const payment = authorizePaymentSessionStep({ + id: "payses_123", + context: {}, +}) +``` + +When the payment authorization is successful, a payment is created and returned. + +#### Handling Additional Action + +If you used the `authorizePaymentSessionStep`, you don't need to implement this logic as it's implemented in the step. + +If the payment authorization isn’t successful, whether because it requires additional action or for another reason, the method updates the payment session with the new status and throws an error. + +In that case, you can catch that error and, if the session's `status` property is `requires_more`, handle the additional action, then retry the authorization. + +For example: + +```ts +try { + const payment = + await paymentModuleService.authorizePaymentSession( + paymentSession.id, + {} + ) +} catch (e) { + // retrieve the payment session again + const updatedPaymentSession = ( + await paymentModuleService.listPaymentSessions({ + id: [paymentSession.id], + }) + )[0] + + if (updatedPaymentSession.status === "requires_more") { + // TODO perform required action + // TODO authorize payment again. + } +} +``` + +### 5. Payment Flow Complete + +The payment flow is complete once the payment session is authorized and the payment is created. + +You can then: + +- Complete the cart using the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) if you're using the Medusa application. +- Capture the payment either using the [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) or [capturePayment method](https://docs.medusajs.com/references/payment/capturePayment/index.html.md). +- Refund captured amounts using the [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) or [refundPayment method](https://docs.medusajs.com/references/payment/refundPayment/index.html.md). + +Some payment providers allow capturing the payment automatically once it’s authorized. In that case, you don’t need to do it manually. + + +# Payment Webhook Events + +In this guide, you’ll learn how you can handle payment webhook events in your Medusa application and using the Payment Module. + +## What's a Payment Webhook Event? + +A payment webhook event is a request sent from a third-party payment provider to your application. It indicates a change in a payment’s status. + +This is useful in many cases such as: + +- When a payment is processed (authorized or captured) asynchronously. +- When a payment is managed on the third-party payment provider's side. +- When a payment action on the frontend was interrupted, leading the payment to be processed without an order being created in the Medusa application. + +So, it's essential to handle webhook events to ensure that your application is aware of updated payment statuses and can take appropriate actions. + +*** + +## How to Handle Payment Webhook Events + +### Webhook Listener API Route + +The Medusa application has a `/hooks/payment/[identifier]_[provider]` API route out-of-the-box that allows you to listen to webhook events from third-party payment providers, where: + +- `[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`. + +You can use this webhook listener when configuring webhook events in your third-party payment provider. + +### getWebhookActionAndData Method + +The webhook listener API route executes the [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/getWebhookActionAndData/index.html.md) of the Payment Module's main service. This method delegates handling of incoming webhook events to the relevant payment provider. + +Payment providers have a similar [getWebhookActionAndData method](https://docs.medusajs.com/references/payment/provider/index.html.md) to process the webhook event. So, if you're implementing a custom payment provider, make sure to implement it to handle webhook events. + +![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) + +If the `getWebhookActionAndData` method returns an `authorized` or `captured` action, the Medusa application will perform one of the following actions: + +View the full flow of the webhook event processing in the [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) reference. + +- If the method returns an `authorized` action, Medusa will set the associated payment session to `authorized`. +- If the method returns a `captured` action, Medusa will set the associated payment session to `captured`. +- In either cases, if the cart associated with the payment session is not completed yet, Medusa will complete the cart. + + +# Payment Module Provider + +In this guide, you’ll learn about the Payment Module Provider and how it's used. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/regions/index.html.md) to learn how to manage the payment providers available in a region using the dashboard. + +*** + +## What is a Payment Module Provider? + +The Payment Module Provider handles payment processing in the Medusa application. It integrates third-party payment services, such as [Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md). + +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. + +![Diagram showcasing the communication between Medusa, the Payment Module Provider, and the third-party payment provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791374/Medusa%20Resources/payment-provider-service_l4zi6m.jpg) + +### List of Payment Module Providers + +- [Stripe](https://docs.medusajs.com/commerce-modules/payment/payment-provider/stripe/index.html.md) + +### Default 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. + +The identifier of the system payment provider is `pp_system`. + +*** + +## How to Create a Custom Payment Provider? + +A payment provider is a module whose main service extends the `AbstractPaymentProvider` imported from `@medusajs/framework/utils`. + +The module can have multiple payment provider services, where each is registered as a separate payment provider. + +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. + +After you create a payment provider, you can enable it as a payment provider in a region using the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/regions/index.html.md). + +*** + +## How are Payment Providers Registered? + +### Configure Payment Module's Providers + +The Payment Module accepts a `providers` option that allows you to configure the providers registered in your application. + +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/module-options/index.html.md) guide. + +### Registration on Application Start + +When the Medusa application starts, it registers the Payment Module Providers defined in the `providers` option of the Payment Module. + +For each Payment Module Provider, the Medusa application finds all payment provider services defined in them to register. + +### PaymentProvider Data Model + +A registered payment provider is represented by the [PaymentProvider data model](https://docs.medusajs.com/references/payment/models/PaymentProvider/index.html.md) in the Medusa application. + +![Diagram showcasing the PaymentProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791364/Medusa%20Resources/payment-provider-model_lx91oa.jpg) + +This data model is used to reference a service in the Payment Module Provider and determine whether it's installed in the application. + +The `PaymentProvider` data model has the following properties: + +- `id`: The unique identifier of the Payment Module Provider. The ID's format is `pp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Payment Module Provider's service. + - `id` is the value of the `id` property of the Payment Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the payment provider is enabled. + +### How to Remove a Payment Provider? + +If you remove a payment provider from the `providers` option, the Medusa application will not remove the associated `PaymentProvider` data model record. + +Instead, the Medusa application will set the `is_enabled` property of the `PaymentProvider`'s record to `false`. This allows you to re-enable the payment provider later if needed by adding it back to the `providers` option. + + +# Payment Session + +In this document, you’ll learn what a payment session is. + +## What's a Payment Session? + +A payment session, represented by the [PaymentSession data model](https://docs.medusajs.com/references/payment/models/PaymentSession/index.html.md), is a payment amount to be authorized. It’s associated with a payment provider that handles authorizing it. + +A payment collection can have multiple payment sessions. Using this feature, you can implement payment in installments or payments using multiple providers. + +![Diagram showcasing how every payment session has a different payment provider](https://res.cloudinary.com/dza7lstvk/image/upload/v1711565056/Medusa%20Resources/payment-session-provider_guxzqt.jpg) + +*** + +## data Property + +Payment providers may need additional data to process the payment later. For example, the ID of the session in the third-party provider. + +The `PaymentSession` data model has a `data` property used to store that data. It's set by the [payment provider in Medusa](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/index.html.md) when the payment is initialized. + +Then, when the payment session is authorized, the `data` property is used by the payment provider in Medusa to process the payment with the third-party provider. + +If you're building a custom payment provider, learn more about initializing the payment session and setting the `data` property in the [Create Payment Provider](https://docs.medusajs.com/references/payment/provider/index.html.md) guide. + +### data Property in the Storefront + +This `data` property is accessible in the storefront as well. So, only store in it data that can be publicly shared, and data that is useful in the storefront. + +For example, you can also store the client token used to initialize the payment session in the storefront with the third-party provider. + +*** + +## Payment Session Status + +The `status` property of a payment session indicates its current status. Its value can be: + +- `pending`: The payment session is awaiting authorization. +- `requires_more`: The payment session requires an action before it’s authorized. For example, to enter a 3DS code. +- `authorized`: The payment session is authorized. +- `error`: An error occurred while authorizing the payment. +- `canceled`: The authorization of the payment session has been canceled. + + # Links between Region Module and Other Modules This document showcases the module links defined between the Region Module and other Commerce Modules. @@ -28619,6 +28619,32 @@ 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. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. + +## What is a Campaign? + +A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. + +![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) + +*** + +## Campaign Limits + +Each campaign has a budget represented by the [CampaignBudget data model](https://docs.medusajs.com/references/promotion/models/CampaignBudget/index.html.md). The budget limits how many times the promotion can be used. + +There are two types of budgets: + +- `spend`: An amount that, when crossed, the promotion becomes unusable. For example, if the amount limit is set to `$100`, and the total amount of usage of this promotion crosses that threshold, the promotion can no longer be applied. +- `usage`: The number of times that a promotion can be used. For example, if the usage limit is set to `10`, the promotion can be used only 10 times by customers. After that, it can no longer be applied. + +![A diagram showcasing the relation between the Campaign and CampaignBudget data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899463/Medusa%20Resources/campagin-budget_rvqlmi.jpg) + + # Promotion Concepts In this guide, you’ll learn about the main promotion and rule concepts in the Promotion Module. @@ -28781,32 +28807,6 @@ For example, consider you have the following promotion with a rule that restrict When you try to apply this promotion on a cart, the cart's `customer_id` is compared to the promotion rule's value based on the specified operator. So, the promotion will only be applied if the cart's `customer_id` is equal to `cus_123`. -# Campaign - -In this document, you'll learn about campaigns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/campaigns/index.html.md) to learn how to manage campaigns using the dashboard. - -## What is a Campaign? - -A [Campaign](https://docs.medusajs.com/references/promotion/models/Campaign/index.html.md) combines promotions under the same conditions, such as start and end dates. - -![A diagram showcasing the relation between the Campaign and Promotion data models](https://res.cloudinary.com/dza7lstvk/image/upload/v1709899225/Medusa%20Resources/campagin-promotion_hh3qsi.jpg) - -*** - -## 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) - - # Links between Promotion Module and Other Modules This document showcases the module links defined between the Promotion Module and other Commerce Modules. @@ -28990,251 +28990,189 @@ createRemoteLinkStep({ ``` -# Stock Location Concepts +# Tax Calculation with the Tax Provider -In this document, you’ll learn about the main concepts in the Stock Location Module. +In this guide, you’ll learn how tax lines are calculated using the tax provider. -## Stock Location +## Tax Lines Calculation -A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. +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. -Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. - -*** - -## StockLocationAddress - -The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. - - -# Links between Stock Location Module and Other Modules - -This document showcases the module links defined between the Stock Location Module and other Commerce Modules. - -## Summary - -The Stock Location Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more| -|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more| -|InventoryLevel|StockLocation|Read-only - has many|Learn more| -|SalesChannel|StockLocation|Stored - many-to-many|Learn more| - -*** - -## Fulfillment Module - -A fulfillment set can be conditioned to a specific stock location. - -Medusa defines a link between the `FulfillmentSet` and `StockLocation` data models. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1712567101/Medusa%20Resources/fulfillment-stock-location_nlkf7e.jpg) - -Medusa also defines a link between the `FulfillmentProvider` and `StockLocation` data models to indicate the providers that can be used in a location. - -![A diagram showcasing an example of how data models from the Fulfillment and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728399492/Medusa%20Resources/fulfillment-provider-stock-location_b0mulo.jpg) - -### Retrieve with Query - -To retrieve the fulfillment sets of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `fulfillment_sets.*` in `fields`: - -To retrieve the fulfillment providers, pass `fulfillment_providers.*` in `fields`. - -### query.graph +For example: ```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", +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, + }, ], -}) - -// stockLocations[0].fulfillment_sets + { + address: { + country_code: "us", + }, + } +) ``` -### useQueryGraphStep +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. -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +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: -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "fulfillment_sets.*", - ], -}) - -// stockLocations[0].fulfillment_sets -``` - -### Manage with Link - -To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.STOCK_LOCATION]: { - stock_location_id: "sloc_123", +```json +[ + { + "line_item_id": "cali_123", + "rate_id": "txr_1", + "rate": 10, + "code": "XXX", + "name": "Tax Rate 1" }, - [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", - }, -}) + { + "shipping_line_id": "casm_123", + "rate_id": "txr_2", + "rate": 5, + "code": "YYY", + "name": "Tax Rate 2" + } +] ``` *** -## Inventory Module +## Using the Tax Provider in the Calculation -Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. +The tax lines retrieved by the `getTaxLines` method are actually retrieved from the tax region’s [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md). -### Retrieve with Query +A tax module implements the logic to shape tax lines. Each tax region uses a tax provider. -To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: +Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. -### query.graph -```ts -const { data: inventoryLevels } = await query.graph({ - entity: "inventory_level", - fields: [ - "stock_locations.*", - ], -}) +# Tax Module Options -// inventoryLevels[0].stock_locations -``` +In this guide, you'll learn about the options of the Tax Module. -### useQueryGraphStep +## providers -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) 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" // ... -const { data: inventoryLevels } = useQueryGraphStep({ - entity: "inventory_level", - fields: [ - "stock_locations.*", +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/tax", + options: { + providers: [ + { + resolve: "./path/to/my-provider", + id: "my-provider", + options: { + // ... + }, + }, + ], + }, + }, ], }) - -// inventoryLevels[0].stock_locations ``` +The objects in the array accept the following properties: + +- `resolve`: A string indicating the package name of the module provider or the path to it. +- `id`: A string indicating the provider's unique name or ID. +- `options`: An optional object of the module provider's options. + + +# Tax Rates and Rules + +In this document, you’ll learn about tax rates and rules. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. + +## What are Tax Rates? + +A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. + +Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. + +### Combinable Tax Rates + +Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. + +Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. + *** -## Sales Channel Module +## Override Tax Rates with Rules -A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. +You can create tax rates that override the default for specific conditions or rules. -Medusa defines a link between the `SalesChannel` and `StockLocation` data models. +For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. -![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 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. -### Retrieve with Query +![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) -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`: +These two properties of the data model identify the rule’s target: -### query.graph +- `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. -```ts -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) +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. -// stockLocations[0].sales_channels -``` -### useQueryGraphStep +# Tax Region -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +In this document, you’ll learn about tax regions and how to use them with the Region Module. -// ... +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. -const { data: stockLocations } = useQueryGraphStep({ - entity: "stock_location", - fields: [ - "sales_channels.*", - ], -}) +## What is a Tax Region? -// stockLocations[0].sales_channels -``` +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. -### Manage with Link +Tax regions can inherit settings and rules from a parent tax region. -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 +## Tax Rules in a Tax Region -```ts -import { Modules } from "@medusajs/framework/utils" +Tax rules define the tax rates and behavior within a tax region. They specify: -// ... +- The tax rate percentage. +- Which products the tax applies to. +- Other custom rules to determine tax applicability. -await link.create({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` +Learn more about tax rules in the [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) guide. -### createRemoteLinkStep +*** -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" +## Tax Provider -// ... +Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region. -createRemoteLinkStep({ - [Modules.SALES_CHANNEL]: { - sales_channel_id: "sc_123", - }, - [Modules.STOCK_LOCATION]: { - sales_channel_id: "sloc_123", - }, -}) -``` +You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic. + +Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. # Publishable API Keys with Sales Channels @@ -29640,43 +29578,71 @@ createRemoteLinkStep({ ``` -# Links between Store Module and Other Modules +# Stock Location Concepts -This document showcases the module links defined between the Store Module and other Commerce Modules. +In this document, you’ll learn about the main concepts in the Stock Location Module. + +## Stock Location + +A stock location, represented by the `StockLocation` data model, represents a location where stock items are kept. For example, a warehouse. + +Medusa uses stock locations to provide inventory details, from the Inventory Module, per location. + +*** + +## StockLocationAddress + +The `StockLocationAddress` data model belongs to the `StockLocation` data model. It provides more detailed information of the location, such as country code or street address. + + +# Links between Stock Location Module and Other Modules + +This document showcases the module links defined between the Stock Location Module and other Commerce Modules. ## Summary -The Store Module has the following links to other modules: +The Stock Location Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|StoreCurrency|Currency|Read-only - has many|Learn more| +|FulfillmentSet|StockLocation|Stored - many-to-one|Learn more| +|FulfillmentProvider|StockLocation|Stored - many-to-many|Learn more| +|InventoryLevel|StockLocation|Read-only - has many|Learn more| +|SalesChannel|StockLocation|Stored - many-to-many|Learn more| *** -## Currency Module +## Fulfillment 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. +A fulfillment set can be conditioned to a specific stock location. -Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). +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 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 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: stores } = await query.graph({ - entity: "store", +const { data: stockLocations } = await query.graph({ + entity: "stock_location", fields: [ - "supported_currencies.currency.*", + "fulfillment_sets.*", ], }) -// stores[0].supported_currencies +// stockLocations[0].fulfillment_sets ``` ### useQueryGraphStep @@ -29686,310 +29652,176 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", fields: [ - "supported_currencies.currency.*", + "fulfillment_sets.*", ], }) -// stores[0].supported_currencies +// stockLocations[0].fulfillment_sets ``` +### Manage with Link -# Tax Module Options +To manage the stock location of a fulfillment set, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): -In this guide, you'll learn about the options of the Tax Module. - -## providers - -The `providers` option is an array of either [tax module providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) 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 guide, you’ll learn how tax lines are calculated using the tax provider. - -## 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: +### 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", - }, - } -) -``` - -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 [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md). - -A tax module implements the logic to shape tax lines. Each tax region uses a tax provider. - -Learn more about tax providers, configuring, and creating them in the [Tax Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. - - -# Tax Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - -# Tax Module Provider - -In this guide, you’ll learn about the Tax Module Provider and how it's used. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard. - -## What is a Tax Module Provider? - -The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic. - -The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md). - -![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg) - -*** - -## Default Tax Provider - -The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide. - -This provider is installed by default in your application and you can use it with tax regions. - -The identifier of the system tax provider is `tp_system`. - -*** - -## How to Create a Custom Tax Provider? - -A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`. - -The module can have multiple tax provider services, where each are registered as separate tax providers. - -Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider. - -After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md). - -*** - -## How are Tax Providers Registered? - -### Configure Tax Module's Providers - -The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application. - -Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide. - -### Registration on Application Start - -When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module. - -For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register. - -### TaxProvider Data Model - -A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application. - -This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application. - -![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg) - -The `TaxProvider` data model has the following properties: - -- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where: - - `identifier` is the value of the `identifier` property in the Tax Module Provider's service. - - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`. -- `is_enabled`: A boolean indicating whether the tax provider is enabled. - -### How to Remove a Tax Provider? - -You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration. - -Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option. - - -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -*** - -## Tax Rules in a Tax Region - -Tax rules define the tax rates and behavior within a tax region. They specify: - -- The tax rate percentage. -- Which products the tax applies to. -- Other custom rules to determine tax applicability. - -Learn more about tax rules in the [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) guide. - -*** - -## Tax Provider - -Each tax region can have a default tax provider. The tax provider is responsible for calculating the tax lines for carts and orders in that region. - -You can use Medusa's default tax provider or create a custom one, allowing you to integrate with third-party tax services or implement your own tax calculation logic. - -Learn more about tax providers in the [Tax Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-provider/index.html.md) guide. - - -# 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, - }, - }, - ], +await link.create({ + [Modules.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_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.STOCK_LOCATION]: { + stock_location_id: "sloc_123", + }, + [Modules.FULFILLMENT]: { + fulfillment_set_id: "fset_123", + }, +}) +``` + +*** + +## Inventory Module + +Medusa defines a read-only link between the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md)'s `InventoryLevel` data model and the `StockLocation` data model. Because the link is read-only from the `InventoryLevel`'s side, you can only retrieve the stock location of an inventory level, and not the other way around. + +### Retrieve with Query + +To retrieve the stock locations of an inventory level with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `stock_locations.*` in `fields`: + +### query.graph + +```ts +const { data: inventoryLevels } = await query.graph({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: inventoryLevels } = useQueryGraphStep({ + entity: "inventory_level", + fields: [ + "stock_locations.*", + ], +}) + +// inventoryLevels[0].stock_locations +``` + +*** + +## Sales Channel Module + +A stock location is associated with a sales channel. This scopes inventory quantities in a stock location by the associated sales channel. + +Medusa defines a link between the `SalesChannel` and `StockLocation` data models. + +![A diagram showcasing an example of how resources from the Sales Channel and Stock Location modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716796872/Medusa%20Resources/sales-channel-location_cqrih1.jpg) + +### Retrieve with Query + +To retrieve the sales channels of a stock location with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channels.*` in `fields`: + +### query.graph + +```ts +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stockLocations } = useQueryGraphStep({ + entity: "stock_location", + fields: [ + "sales_channels.*", + ], +}) + +// stockLocations[0].sales_channels +``` + +### Manage with Link + +To manage the stock locations of a sales channel, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.SALES_CHANNEL]: { + sales_channel_id: "sc_123", + }, + [Modules.STOCK_LOCATION]: { + sales_channel_id: "sloc_123", + }, +}) ``` @@ -30073,6 +29905,100 @@ if (!count) { ``` +# 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 +``` + + +# Links between Store Module and Other Modules + +This document showcases the module links defined between the Store Module and other Commerce Modules. + +## Summary + +The Store Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|StoreCurrency|Currency|Read-only - has many|Learn more| + +*** + +## Currency Module + +The Store Module has a `Currency` data model that stores the supported currencies of a store. However, these currencies don't hold all the details of a currency, such as its name or symbol. + +Instead, Medusa defines a read-only link between the [Currency Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/currency/index.html.md)'s `Currency` data model and the Store Module's `StoreCurrency` data model. This means you can retrieve the details of a store's supported currencies, but you don't manage the links in a pivot table in the database. The currencies of a store are determined by the `currency_code` of the [Currency](https://docs.medusajs.com/references/store/models/Currency/index.html.md) data model in the Store Module (not in the Currency Module). + +### Retrieve with Query + +To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: + +### query.graph + +```ts +const { data: stores } = await query.graph({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: [ + "supported_currencies.currency.*", + ], +}) + +// stores[0].supported_currencies +``` + + # 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. @@ -30135,303 +30061,78 @@ const hashConfig = \{ - [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) -# GitHub Auth Module Provider +# Tax Module Provider -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. +In this guide, you’ll learn about the Tax Module Provider and how it's used. -The Github Auth Module Provider authenticates users with their GitHub account. +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax provider of a tax region using the dashboard. -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). +## What is a Tax Module Provider? + +The Tax Module Provider handles tax line calculations in the Medusa application. It integrates third-party tax services, such as TaxJar, or implements custom tax calculation logic. + +The Medusa application uses the Tax Module Provider whenever it needs to calculate tax lines for a cart or order, or when you [calculate the tax lines using the Tax Module's service](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/tax-calculation-with-provider/index.html.md). + +![Diagram showcasing the communication between Medusa the Tax Module Provider, and the third-party tax provider.](https://res.cloudinary.com/dza7lstvk/image/upload/v1746790996/Medusa%20Resources/tax-provider-service_kcgpne.jpg) *** -## Register the Github Auth Module Provider +## Default Tax Provider -### Prerequisites +The Tax Module provides a `system` tax provider that acts as a placeholder tax provider. It performs basic tax calculation, as you can see in the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider#gettaxlines/index.html.md) guide. -- [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) +This provider is installed by default in your application and you can use it with tax regions. -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| +The identifier of the system tax provider is `tp_system`. *** -## Override Callback URL During Authentication +## How to Create a Custom Tax Provider? -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. +A Tax Module Provider is a module whose service implements the `ITaxProvider` imported from `@medusajs/framework/types`. -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). +The module can have multiple tax provider services, where each are registered as separate tax providers. + +Refer to the [Create Tax Module Provider](https://docs.medusajs.com/references/tax/provider/index.html.md) guide to learn how to create a Tax Module Provider. + +After you create a tax provider, you can choose it as the default Tax Module Provider for a region in the [Medusa Admin dashboard](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md). *** -## Examples +## How are Tax Providers Registered? -- [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). +### Configure Tax Module's Providers +The Tax Module accepts a `providers` option that allows you to configure the providers registered in your application. -# Google Auth Module Provider +Learn more about this option in the [Module Options](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/tax/module-options/index.html.md) guide. -In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. +### Registration on Application Start -The Google Auth Module Provider authenticates users with their Google account. +When the Medusa application starts, it registers the Tax Module Providers defined in the `providers` option of the Tax Module. -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). +For each Tax Module Provider, the Medusa application finds all tax provider services defined in them to register. -*** +### TaxProvider Data Model -## Register the Google Auth Module Provider +A registered tax provider is represented by the [TaxProvider data model](https://docs.medusajs.com/references/tax/models/TaxProvider/index.html.md) in the Medusa application. -### Prerequisites +This data model is used to reference a service in the Tax Module Provider and determine whether it's installed in the application. -- [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) +![Diagram showcasing the TaxProvider data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1746791254/Medusa%20Resources/tax-provider-model_r6ktjw.jpg) -Add the module to the array of providers passed to the Auth Module: +The `TaxProvider` data model has the following properties: -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +- `id`: The unique identifier of the tax provider. The ID's format is `tp_{identifier}_{id}`, where: + - `identifier` is the value of the `identifier` property in the Tax Module Provider's service. + - `id` is the value of the `id` property of the Tax Module Provider in `medusa-config.ts`. +- `is_enabled`: A boolean indicating whether the tax provider is enabled. -// ... +### How to Remove a Tax Provider? -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, - }, - }, - ], - }, - }, - }, - ], -}) -``` +You can remove a registered tax provider from the Medusa application by removing it from the `providers` option in the Tax Module's configuration. -### 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). - - -# Stripe Module Provider - -In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. - -Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. - -## 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" -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|-| - -*** - -## Enable Stripe Providers in a Region - -Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. - -Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. - -*** - -## Stripe Payment Provider IDs - -When you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more. - -Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where: - -- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider. -- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`. - -Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers: - -|Provider Name|Provider ID| -|---|---|---| -|Basic Stripe Payment|\`pp\_stripe\_stripe\`| -|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`| -|BLIK Payments|\`pp\_stripe-blik\_stripe\`| -|giropay Payments|\`pp\_stripe-giropay\_stripe\`| -|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`| - -*** - -## 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 as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix. - -The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: - -|Stripe Payment Type|Webhook Endpoint URL| -|---|---|---| -|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| -|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| -|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| -|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| -|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| -|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| -|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| - -### 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). -- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md). +Then, the next time the Medusa application starts, it will set the `is_enabled` property of the `TaxProvider`'s record to `false`. This allows you to re-enable the tax provider later if needed by adding it back to the `providers` option. # Get Product Variant Prices using Query @@ -30699,6 +30400,136 @@ For each product variant, you: - `priceWithoutTax`: The variant's price without taxes applied. +# Stripe Module Provider + +In this document, you’ll learn about the Stripe Module Provider and how to configure it in the Payment Module. + +Your technical team must install the Stripe Module Provider in your Medusa application first. Then, refer to [this user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to enable the Stripe payment provider in a region using the Medusa Admin dashboard. + +## 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" +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|-| + +*** + +## Enable Stripe Providers in a Region + +Before customers can use Stripe to complete their purchases, you must enable the Stripe payment provider(s) in the region where you want to offer this payment method. + +Refer to the [user guide](https://docs.medusajs.com/user-guide/settings/regions#edit-region-details/index.html.md) to learn how to edit a region and enable the Stripe payment provider. + +*** + +## Stripe Payment Provider IDs + +When you register the Stripe Module Provider, it registers different providers, such as basic Stripe payment, Bancontact, and more. + +Each provider is registered and referenced by a unique ID made up of the format `pp_{identifier}_{id}`, where: + +- `{identifier}` is the ID of the payment provider as defined in the Stripe Module Provider. +- `{id}` is the ID of the Stripe Module Provider as set in the `medusa-config.ts` file. For example, `stripe`. + +Assuming you set the ID of the Stripe Module Provider to `stripe` in `medusa-config.ts`, the Medusa application will register the following payment providers: + +|Provider Name|Provider ID| +|---|---|---| +|Basic Stripe Payment|\`pp\_stripe\_stripe\`| +|Bancontact Payments|\`pp\_stripe-bancontact\_stripe\`| +|BLIK Payments|\`pp\_stripe-blik\_stripe\`| +|giropay Payments|\`pp\_stripe-giropay\_stripe\`| +|iDEAL Payments|\`pp\_stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`pp\_stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`pp\_stripe-promptpay\_stripe\`| + +*** + +## 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 as explained in the [Stripe Payment Provider IDs](#stripe-payment-provider-ids) section, without the `pp_` prefix. + +The Stripe Module Provider supports the following payment types, and the webhook endpoint URL is different for each: + +|Stripe Payment Type|Webhook Endpoint URL| +|---|---|---| +|Basic Stripe Payment|\`\{server\_url}/hooks/payment/stripe\_stripe\`| +|Bancontact Payments|\`\{server\_url}/hooks/payment/stripe-bancontact\_stripe\`| +|BLIK Payments|\`\{server\_url}/hooks/payment/stripe-blik\_stripe\`| +|giropay Payments|\`\{server\_url}/hooks/payment/stripe-giropay\_stripe\`| +|iDEAL Payments|\`\{server\_url}/hooks/payment/stripe-ideal\_stripe\`| +|Przelewy24 Payments|\`\{server\_url}/hooks/payment/stripe-przelewy24\_stripe\`| +|PromptPay Payments|\`\{server\_url}/hooks/payment/stripe-promptpay\_stripe\`| + +### 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). +- [Add Saved Payment Methods with Stripe](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/saved-payment-methods/index.html.md). + + # Get Product Variant Inventory Quantity In this guide, you'll learn how to retrieve the available inventory quantity of a product variant in your Medusa application customizations. That includes API routes, workflows, subscribers, scheduled jobs, and any resource that can access the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). @@ -30852,67 +30683,262 @@ You pass the ID of the variant as a filter, and you specify `product.sales_chann Then, you pass the first sales channel ID to the `getVariantAvailability` function to retrieve the inventory availability of the product variant in that sales channel. +# 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). + + +# 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). + + ## Workflows +- [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) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/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) - [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) - [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) - [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) - [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) - [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) -- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) -- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) -- [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) -- [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) -- [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) -- [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) -- [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) - [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) - [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) - [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) - [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) - [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) - [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) - [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/index.html.md) -- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) - [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/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) - [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) - [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [updateCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCartWorkflow/index.html.md) - [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) +- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) +- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/index.html.md) +- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) +- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) +- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) +- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) +- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) +- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) +- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) +- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) +- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) +- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) +- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) +- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md) +- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) +- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md) +- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) +- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) +- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) +- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) +- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) +- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) - [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) +- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) - [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) - [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) -- [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/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) +- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) +- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) +- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/index.html.md) - [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) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) +- [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) - [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) -- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) -- [deleteFulfillmentSetsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFulfillmentSetsWorkflow/index.html.md) +- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/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) -- [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) -- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) +- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/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) +- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/index.html.md) +- [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) - [updateShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingProfilesWorkflow/index.html.md) - [validateFulfillmentDeliverabilityStep](https://docs.medusajs.com/references/medusa-workflows/validateFulfillmentDeliverabilityStep/index.html.md) @@ -30921,267 +30947,264 @@ Then, you pass the first sales channel ID to the `getVariantAvailability` functi - [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) - [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) - [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/index.html.md) -- [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) - [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) -- [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) -- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) -- [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) -- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) -- [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) -- [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) -- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) -- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [removeDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionItemWorkflow/index.html.md) -- [removeDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderActionShippingMethodWorkflow/index.html.md) -- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) -- [removeDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderShippingMethodWorkflow/index.html.md) -- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) -- [updateDraftOrderActionItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionItemWorkflow/index.html.md) -- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) -- [updateDraftOrderItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderItemWorkflow/index.html.md) -- [updateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderStep/index.html.md) -- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/index.html.md) -- [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [refreshInviteTokensWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshInviteTokensWorkflow/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) -- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) -- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) -- [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) -- [deletePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePricePreferencesWorkflow/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) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/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) -- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) -- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/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) -- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) -- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) -- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) -- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) -- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/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) - [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/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) -- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) +- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) - [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) - [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) - [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) - [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) - [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/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) -- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) - [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) - [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) - [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) - [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) - [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) +- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) - [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/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) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) - [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) -- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) -- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) - [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/index.html.md) +- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) - [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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) - [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) - [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/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) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) - [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) - [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) - [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) - [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) - [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/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) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) - [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) +- [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) -- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/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) +- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/index.html.md) - [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) -- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) - [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) +- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) +- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/index.html.md) - [createOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodWorkflow/index.html.md) - [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) - [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) - [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) - [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) +- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) +- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) - [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) - [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) +- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) - [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) -- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) - [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) +- [deleteOrderPaymentCollections](https://docs.medusajs.com/references/medusa-workflows/deleteOrderPaymentCollections/index.html.md) - [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/index.html.md) - [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md) - [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) +- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md) - [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) -- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) - [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) -- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md) - [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md) +- [markPaymentCollectionAsPaid](https://docs.medusajs.com/references/medusa-workflows/markPaymentCollectionAsPaid/index.html.md) - [orderClaimAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemWorkflow/index.html.md) -- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/index.html.md) -- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/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) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) - [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) +- [orderClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemWorkflow/index.html.md) +- [orderClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimItemValidationStep/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) -- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) -- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/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) - [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) -- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) - [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) - [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) - [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) - [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) - [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/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) - [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) - [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) - [removeExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodValidationStep/index.html.md) -- [removeItemExchangeActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemExchangeActionWorkflow/index.html.md) - [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/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) - [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) -- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) - [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) - [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) - [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) -- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) +- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) - [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/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) +- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) - [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) -- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) - [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) +- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/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) +- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) - [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/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) - [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) - [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) - [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/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) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/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) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) - [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) - [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) +- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) +- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) - [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) - [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) - [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) +- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) - [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) - [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) - [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) +- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) +- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) - [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) - [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [createProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductCategoriesWorkflow/index.html.md) +- [deleteProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductCategoriesWorkflow/index.html.md) +- [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/index.html.md) +- [capturePaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/capturePaymentWorkflow/index.html.md) +- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/index.html.md) +- [processPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/processPaymentWorkflow/index.html.md) +- [refundPaymentsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentsWorkflow/index.html.md) +- [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) +- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) +- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) +- [deletePriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePriceListsWorkflow/index.html.md) +- [updatePriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePriceListPricesWorkflow/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) - [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) - [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) - [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) - [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) -- [batchLinkProductsToCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCollectionWorkflow/index.html.md) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/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) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) - [createCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCollectionsWorkflow/index.html.md) +- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/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) - [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md) - [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - [deleteCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCollectionsWorkflow/index.html.md) +- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) - [deleteProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTypesWorkflow/index.html.md) - [deleteProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductTagsWorkflow/index.html.md) -- [deleteProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductOptionsWorkflow/index.html.md) - [deleteProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteProductVariantsWorkflow/index.html.md) -- [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/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) - [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/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) -- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) -- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) -- [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) -- [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) - [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/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) - [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) +- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) +- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) +- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) +- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) +- [batchPromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPromotionRulesWorkflow/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) +- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/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) +- [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) +- [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/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) +- [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) +- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) +- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) +- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) - [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) - [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) - [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) - [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [deleteShippingProfileWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingProfileWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/index.html.md) -- [createStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStoresWorkflow/index.html.md) -- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) -- [updateStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStoresWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) +- [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) +- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) +- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/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) +- [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) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) - [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) @@ -31190,293 +31213,270 @@ Then, you pass the first sales channel ID to the `getVariantAvailability` functi - [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) -- [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) -- [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) -- [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/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) -- [updatePromotionsValidationStep](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsValidationStep/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) -- [createUserAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createUserAccountWorkflow/index.html.md) -- [deleteUsersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteUsersWorkflow/index.html.md) -- [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) -- [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) -- [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) -- [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) ## Steps -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) +- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) +- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) +- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) +- [updateRemoteLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRemoteLinksStep/index.html.md) +- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) +- [useRemoteQueryStep](https://docs.medusajs.com/references/medusa-workflows/steps/useRemoteQueryStep/index.html.md) +- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) - [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) +- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) - [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) - [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/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) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) - [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/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) +- [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) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/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) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) - [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) - [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) - [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) - [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) - [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) - [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) +- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/index.html.md) +- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) - [validateCartShippingOptionsPriceStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsPriceStep/index.html.md) - [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/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) - [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) -- [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [dismissRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/dismissRemoteLinkStep/index.html.md) -- [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) -- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/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) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) -- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/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) - [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) - [deleteApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteApiKeysStep/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) - [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) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/index.html.md) -- [createCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomerAddressesStep/index.html.md) -- [deleteCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerAddressesStep/index.html.md) -- [maybeUnsetDefaultShippingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultShippingAddressesStep/index.html.md) -- [maybeUnsetDefaultBillingAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/maybeUnsetDefaultBillingAddressesStep/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) -- [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) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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) -- [linkCustomerGroupsToCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomerGroupsToCustomerStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) - [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/index.html.md) -- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) - [uploadFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/uploadFilesStep/index.html.md) +- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) +- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [validateTokenStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateTokenStep/index.html.md) +- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) - [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) -- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) - [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) +- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) - [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) - [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) -- [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) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) - [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) - [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) - [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/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) - [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) -- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) +- [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) - [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) +- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) +- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) - [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/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) -- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) -- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) +- [listLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listLineItemsStep/index.html.md) +- [deleteLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteLineItemsStep/index.html.md) +- [updateLineItemsStepWithSelector](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStepWithSelector/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) +- [deleteInventoryItemStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInventoryItemStep/index.html.md) - [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/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) - [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) - [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) -- [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) -- [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) -- [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) -- [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) +- [notifyOnFailureStep](https://docs.medusajs.com/references/medusa-workflows/steps/notifyOnFailureStep/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) -- [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) - [cancelOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderChangeStep/index.html.md) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) +- [cancelOrderExchangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderExchangeStep/index.html.md) +- [cancelOrderClaimStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderClaimStep/index.html.md) - [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) - [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) - [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [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) - [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) - [createOrderClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimsStep/index.html.md) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) - [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) - [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) +- [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/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) -- [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) - [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) +- [deleteOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangeActionsStep/index.html.md) - [deleteOrderLineItems](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderLineItems/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) -- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) -- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) - [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReturnsStep/index.html.md) +- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) +- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/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) - [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) - [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) - [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [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) -- [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) +- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) - [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/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) -- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) - [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) -- [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) +- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) - [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) +- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) +- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) +- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) +- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) +- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) +- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) +- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) +- [updatePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesStep/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) +- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) - [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) - [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) -- [getExistingPriceListsPriceIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getExistingPriceListsPriceIdsStep/index.html.md) -- [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) - [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [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) -- [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) +- [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) +- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) +- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) +- [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/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) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) +- [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) +- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) +- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) +- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) +- [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) +- [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) +- [getProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getProductsStep/index.html.md) +- [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) +- [groupProductsForBatchStep](https://docs.medusajs.com/references/medusa-workflows/steps/groupProductsForBatchStep/index.html.md) +- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md) +- [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) +- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) +- [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/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) +- [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) +- [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) +- [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) - [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) - [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) - [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) -- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) - [deleteCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCampaignsStep/index.html.md) +- [createPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPromotionsStep/index.html.md) - [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) - [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) +- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) - [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) - [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/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) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/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) -- [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) -- [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) -- [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/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) -- [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) -- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md) -- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/index.html.md) -- [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) -- [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) -- [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) -- [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) +- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) +- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) +- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) +- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) +- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) - [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) +- [updateReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnReasonsStep/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) - [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) - [associateProductsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateProductsWithSalesChannelsStep/index.html.md) - [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) -- [detachProductsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachProductsFromSalesChannelsStep/index.html.md) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/index.html.md) +- [createDefaultSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultSalesChannelStep/index.html.md) - [detachLocationsFromSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/detachLocationsFromSalesChannelsStep/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) -- [createRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRegionsStep/index.html.md) -- [deleteRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRegionsStep/index.html.md) -- [setRegionsPaymentProvidersStep](https://docs.medusajs.com/references/medusa-workflows/steps/setRegionsPaymentProvidersStep/index.html.md) -- [updateRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRegionsStep/index.html.md) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) -- [createReturnReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnReasonsStep/index.html.md) -- [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) - [deleteShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingProfilesStep/index.html.md) +- [listShippingOptionsForContextStep](https://docs.medusajs.com/references/medusa-workflows/steps/listShippingOptionsForContextStep/index.html.md) +- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) +- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) +- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) +- [deleteStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStoresStep/index.html.md) +- [createStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/createStoresStep/index.html.md) +- [updateStoresStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStoresStep/index.html.md) - [createTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRateRulesStep/index.html.md) - [createTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRatesStep/index.html.md) - [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/index.html.md) - [deleteTaxRateRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRateRulesStep/index.html.md) - [deleteTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRatesStep/index.html.md) -- [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) - [deleteTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteTaxRegionsStep/index.html.md) +- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) - [listTaxRateIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateIdsStep/index.html.md) -- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) +- [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) - [updateTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRegionsStep/index.html.md) -- [createStockLocations](https://docs.medusajs.com/references/medusa-workflows/steps/createStockLocations/index.html.md) -- [deleteStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteStockLocationsStep/index.html.md) -- [updateStockLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateStockLocationsStep/index.html.md) -- [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) +- [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) - [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) -- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) - [updateUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateUsersStep/index.html.md) +- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/index.html.md) # Events Reference @@ -32920,84 +32920,6 @@ npx medusa --help *** -# build Command - Medusa CLI Reference - -Create a standalone build of the Medusa application. - -This creates a build that: - -- Doesn't rely on the source TypeScript files. -- Can be copied to a production server reliably. - -The build is outputted to a new `.medusa/server` directory. - -```bash -npx medusa build -``` - -Refer to [this section](#run-built-medusa-application) for next steps. - -## Options - -|Option|Description| -|---|---|---| -|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | - -*** - -## Run Built Medusa Application - -After running the `build` command, use the following step to run the built Medusa application: - -- Change to the `.medusa/server` directory and install the dependencies: - -```bash npm2yarn -cd .medusa/server && npm install -``` - -- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. - -```bash npm2yarn -cp .env .medusa/server/.env.production -``` - -- In the system environment variables, set `NODE_ENV` to `production`: - -```bash -NODE_ENV=production -``` - -- Use the `start` command to run the application: - -```bash npm2yarn -cd .medusa/server && npm run start -``` - -*** - -## Build Medusa Admin - -By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. - -If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. - - -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -33118,6 +33040,22 @@ 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\`| + + # 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). @@ -33163,6 +33101,85 @@ medusa new [ []] |\`--db-host \\`|The database host to use for database setup.| +# start Command - Medusa CLI Reference + +Start the Medusa application in production. + +```bash +npx medusa start +``` + +## Options + +|Option|Description|Default| +|---|---|---|---|---| +|\`-H \\`|Set host of the Medusa server.|\`localhost\`| +|\`-p \\`|Set port of the Medusa server.|\`9000\`| +|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| + + +# build Command - Medusa CLI Reference + +Create a standalone build of the Medusa application. + +This creates a build that: + +- Doesn't rely on the source TypeScript files. +- Can be copied to a production server reliably. + +The build is outputted to a new `.medusa/server` directory. + +```bash +npx medusa build +``` + +Refer to [this section](#run-built-medusa-application) for next steps. + +## Options + +|Option|Description| +|---|---|---| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | + +*** + +## Run Built Medusa Application + +After running the `build` command, use the following step to run the built Medusa application: + +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install +``` + +- When running the application locally, make sure to copy the `.env` file from the root project's directory. In production, use system environment variables instead. + +```bash npm2yarn +cp .env .medusa/server/.env.production +``` + +- In the system environment variables, set `NODE_ENV` to `production`: + +```bash +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start +``` + +*** + +## Build Medusa Admin + +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. + +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. + + # 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. @@ -33224,39 +33241,6 @@ npx medusa plugin:build ``` -# telemetry Command - Medusa CLI Reference - -Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. - -```bash -npx medusa telemetry -``` - -#### Options - -|Option|Description| -|---|---|---| -|\`--enable\`|Enable telemetry (default).| -|\`--disable\`|Disable telemetry.| - - -# start Command - Medusa CLI Reference - -Start the Medusa application in production. - -```bash -npx medusa start -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| -|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| - - # user Command - Medusa CLI Reference Create a new admin user. @@ -33276,6 +33260,22 @@ npx medusa user --email [--password ] If ran successfully, you'll receive the invite token in the output.|No|\`false\`| +# telemetry Command - Medusa CLI Reference + +Enable or disable the collection of anonymous data usage. If no option is provided, the command enables the collection of anonymous data usage. + +```bash +npx medusa telemetry +``` + +#### Options + +|Option|Description| +|---|---|---| +|\`--enable\`|Enable telemetry (default).| +|\`--disable\`|Disable telemetry.| + + # Medusa CLI Reference The Medusa CLI tool provides commands that facilitate your development. @@ -33361,38 +33361,6 @@ By default, the Medusa Admin is built to the `.medusa/server/public/admin` direc If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. -# develop Command - Medusa CLI Reference - -Start Medusa application in development. This command watches files for any changes, then rebuilds the files and restarts the Medusa application. - -```bash -npx medusa develop -``` - -## Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`-H \\`|Set host of the Medusa server.|\`localhost\`| -|\`-p \\`|Set port of the Medusa server.|\`9000\`| - - -# 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| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -33542,6 +33510,38 @@ 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. + +```bash +npx medusa develop +``` + +## 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. @@ -33603,6 +33603,23 @@ 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\`| +|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -33638,23 +33655,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\`| -|\`--cluster \\`|Start Medusa's Node.js server in |Cluster mode is disabled by default. If the option is passed but no number is passed, Medusa will try to consume all available CPU cores.| - - # Medusa JS SDK In this documentation, you'll learn how to install and use Medusa's JS SDK. @@ -43425,6 +43425,850 @@ If you are new to Medusa, check out the [main documentation](https://docs.medusa To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# Use Saved Payment Methods During Checkout + +In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. + +Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use. + +In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers. + +## Summary + +By following this tutorial, you'll learn how to: + +- Install and set up Medusa and the Next.js Starter Storefront. +- Set up the Stripe Module Provider in Medusa. +- Customize the checkout flow to save customers' payment methods. +- Allow customers to select saved payment methods during checkout. + +![Diagram illustrating the features of this guide](https://res.cloudinary.com/dza7lstvk/image/upload/v1745309355/Medusa%20Resources/saved-payment-methods_orjnix.jpg) + +[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes. + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Set Up the Stripe Module Provider + +Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services. + +The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely. + +In this section, you'll set up the Stripe Module Provider in your Medusa application. + +### Prerequisites + +- [Stripe account](https://stripe.com/) +- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) + +### Register the Stripe Module Provider + +To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/payment", + options: { + providers: [ + { + resolve: "@medusajs/medusa/payment-stripe", + id: "stripe", + options: { + apiKey: process.env.STRIPE_API_KEY, + }, + }, + ], + }, + }, + ], +}) +``` + +The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider. + +You register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties: + +- `resolve`: The package name of the provider. +- `id`: The ID of the provider. This is used to identify the provider in the Medusa application. +- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider. + +Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation. + +### Add Environment Variables + +Next, add the following environment variables to your `.env` file: + +```plain +STRIPE_API_KEY=sk_... +``` + +Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys. + +![Secret API Key in the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745313823/Medusa%20Resources/Screenshot_2025-04-22_at_12.20.08_PM_w6rxxo.png) + +### Enable Stripe in a Region + +In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region. + +To enable it in a region, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Then, go to `localhost:9000/app` and log in with the user you created earlier. + +Once you're logged in: + +1. Go to Settings -> Regions. +2. Click on the region where you want to enable the payment provider. +3. Click the icon at the top right of the first section +4. Choose "Edit" from the dropdown menu +5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown. +6. Once you're done, click the "Save" button. + +Stripe will now be available as a payment option during checkout. + +The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well. + +![Stripe in the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1745245433/Medusa%20Resources/Screenshot_2025-04-21_at_5.23.07_PM_yd7xji.png) + +### Add Evnironement Variable to Storefront + +The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region. + +The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. + +So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory: + +```bash +cd ../medusa-payment-storefront # change based on your project name +``` + +In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`: + +```plain badgeLabel="Storefront" badgeColor="blue" +NEXT_PUBLIC_STRIPE_KEY=pk_123... +``` + +Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys. + +*** + +## Step 3: List Payment Methods API Route + +The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider. + +![Diagram illustrating the relation between customers and account holders in Medusa, and customers in Stripe](https://res.cloudinary.com/dza7lstvk/image/upload/v1745314344/Medusa%20Resources/customer-account-stripe_in5ei6.jpg) + +While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients. + +In this step, you'll create an API route that lists the saved payment methods for an authenticated customer. + +Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more. + +### Create API Route + +An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets. + +So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content: + +```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights} +import { MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { account_holder_id } = req.params + const query = req.scope.resolve("query") + const paymentModuleService = req.scope.resolve("payment") + + const { data: [accountHolder] } = await query.graph({ + entity: "account_holder", + fields: [ + "data", + "provider_id", + ], + filters: { + id: account_holder_id, + }, + }) + + if (!accountHolder) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Account holder not found" + ) + } + + const paymentMethods = await paymentModuleService.listPaymentMethods( + { + provider_id: accountHolder.provider_id, + context: { + account_holder: { + data: { + id: accountHolder.data.id, + }, + }, + }, + } + ) + + res.json({ + payment_methods: paymentMethods, + }) +} +``` + +Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as body parameters or authenticated customer details. +2. A response object to manipulate and send the response. + +The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route. + +You use the Medusa container to resolve: + +- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application. +- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers. + +You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error. + +Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties: + +- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider. +- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider. + +Finally, you return the payment methods in the response. + +### Protect API Route + +Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it. + +To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function. + +Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more. + +Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content: + +```ts title="src/api/middlewares.ts" +import { authenticate, defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/payment-methods/:provider_id/:account_holder_id", + method: "GET", + middlewares: [ + authenticate("customer", ["bearer", "session"]), + ], + }, + ], +}) +``` + +The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties: + +- `matcher`: The path of the API route to apply the middleware to. +- `method`: The HTTP method of the API route to apply the middleware to. +- `middlewares`: An array of middlewares to apply to the API route. + +You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route. + +Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware. + +Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps. + +*** + +## Step 4: Save Payment Methods During Checkout + +In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout. + +The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. + +So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory: + +```bash +cd ../medusa-payment-storefront # change based on your project name +``` + +During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route. + +Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider. + +So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use. + +In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property: + +```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" +// update in two places +await initiatePaymentSession(cart, { + // ... + data: { + setup_future_usage: "off_session", + }, +}) +``` + +You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout. + +By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it. + +### Test it Out + +To test it out, start the Medusa application by running the following command in the Medusa application's directory: + +```bash npm2yarn +npm run dev +``` + +Then, start the Next.js Starter Storefront by running the following command in the storefront's directory: + +```bash npm2yarn +npm run dev +``` + +You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right. + +After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`. + +Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by: + +1. Going to the "Customers" section in the Stripe dashboard. +2. Clicking on the customer you just placed the order with. +3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order. + +![Saved payment method on the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745249064/Medusa%20Resources/Screenshot_2025-04-21_at_6.24.00_PM_peotrd.png) + +In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them. + +*** + +## Step 5: Use Saved Payment Methods During Checkout + +In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order. + +### Retrieve Saved Payment Methods + +To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier. + +Add the following in `src/lib/data/payment.ts`: + +```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights} +export type SavedPaymentMethod = { + id: string + provider_id: string + data: { + card: { + brand: string + last4: string + exp_month: number + exp_year: number + } + } +} + +export const getSavedPaymentMethods = async (accountHolderId: string) => { + const headers = { + ...(await getAuthHeaders()), + } + + return sdk.client.fetch<{ + payment_methods: SavedPaymentMethod[] + }>( + `/store/payment-methods/${accountHolderId}`, + { + method: "GET", + headers, + } + ).catch(() => { + return { + payment_methods: [], + } + }) +} +``` + +You define a type for the retrieved payment methods. It contains the following properties: + +- `id`: The ID of the payment method in the third-party provider. +- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID. +- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers. + +You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods. + +### Add Saved Payment Methods Component + +Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them. + +The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file. + +Start by adding the following import statements at the top of the file: + +```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import { Button } from "@medusajs/ui" +import { useEffect, useState } from "react" +import { HttpTypes } from "@medusajs/types" +import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment" +import { initiatePaymentSession } from "../../../../lib/data/cart" +import { capitalize } from "lodash" +``` + +Then, update the `PaymentContainerProps` type to include the payment session and cart details: + +```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +type PaymentContainerProps = { + // ... + paymentSession?: HttpTypes.StorePaymentSession + cart: HttpTypes.StoreCart +} +``` + +You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method. + +Next, add the following component at the end of the file: + +```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights} +const StripeSavedPaymentMethodsContainer = ({ + paymentSession, + setCardComplete, + setCardBrand, + setError, + cart, +}: { + paymentSession?: HttpTypes.StorePaymentSession + setCardComplete: (complete: boolean) => void + setCardBrand: (brand: string) => void + setError: (error: string | null) => void + cart: HttpTypes.StoreCart +}) => { + const [savedPaymentMethods, setSavedPaymentMethods] = useState< + SavedPaymentMethod[] + >([]) + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState< + string | undefined + >( + paymentSession?.data?.payment_method_id as string | undefined + ) + + useEffect(() => { + const accountHolderId = ( + paymentSession?.context?.account_holder as Record + ) + ?.id + + if (!accountHolderId) { + return + } + + getSavedPaymentMethods(accountHolderId) + .then(({ payment_methods }) => { + setSavedPaymentMethods(payment_methods) + }) + }, [paymentSession]) + + useEffect(() => { + if (!selectedPaymentMethod || !savedPaymentMethods.length) { + setCardComplete(false) + setCardBrand("") + setError(null) + return + } + const selectedMethod = savedPaymentMethods.find( + (method) => method.id === selectedPaymentMethod + ) + + if (!selectedMethod) { + return + } + + setCardBrand(capitalize(selectedMethod.data.card.brand)) + setCardComplete(true) + setError(null) + }, [selectedPaymentMethod, savedPaymentMethods]) + + const handleSelect = async (method: SavedPaymentMethod) => { + // initiate a new payment session with the selected payment method + await initiatePaymentSession(cart, { + provider_id: method.provider_id, + data: { + payment_method_id: method.id, + }, + }).catch((error) => { + setError(error.message) + }) + + setSelectedPaymentMethod(method.id) + } + + if (!savedPaymentMethods.length) { + return <> + } + + // TODO add return statement +} +``` + +You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props: + +- `paymentSession`: The cart's current payment session. +- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow. +- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow. +- `setError`: A function to set the error message in case of an error. +- `cart`: The cart's details. + +In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method. + +Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods. + +You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly. + +Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method. + +To show the saved payment methods, replace the `TODO` with the following `return` statement: + +```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+ + Choose a saved payment method: + + {savedPaymentMethods.map((method) => ( +
handleSelect(method)} + > +
+ { + if (e.target.checked) { + handleSelect(method) + } + }} + /> +
+ + {capitalize(method.data.card.brand)} •••• {method.data.card.last4} + + + Expires {method.data.card.exp_month}/{method.data.card.exp_year} + +
+
+
+ ))} +
+) +``` + +You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method. + +### Modify Existing Stripe Element + +Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one. + +In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component: + +```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +export const StripeCardContainer = ({ + // ... + paymentSession, + cart, +}: Omit & { + // ... +}) => { + // ... +} +``` + +Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one: + +```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState( + paymentSession?.data?.payment_method_id !== undefined +) +``` + +Next, add a function that resets the payment session when the customer switches between saved and new payment methods: + +```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" +const handleRefreshSession = async () => { + await initiatePaymentSession(cart, { + provider_id: paymentProviderId, + }) + setIsUsingSavedPaymentMethod(false) +} +``` + +This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable. + +Finally, replace the `return` statement of the `StripeCardContainer` component with the following: + +```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights} +return ( + + {selectedPaymentOptionId === paymentProviderId && + (stripeReady ? ( +
+ + {isUsingSavedPaymentMethod && ( + + )} + {!isUsingSavedPaymentMethod && ( + <> + + Enter your card details: + + { + setCardBrand( + e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1) + ) + setError(e.error?.message || null) + setCardComplete(e.complete) + }} + /> + + )} +
+ ) : ( + + ))} +
+) +``` + +You update the `return` statement to: + +- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component. +- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element. +- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method. + +The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details. + +Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props. + +In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props: + +```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]} +
+ {isStripeFunc(paymentMethod.id) ? ( + + ) : ( + + )} +
+``` + +### Support Updating Stripe's Client Secret + +The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session. + +With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted. + +To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session. + +In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop: + +```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} + + {children} + +``` + +You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes. + +### Support Payment with Saved Payment Method + +The last change you need to make ensures that the customer can place an order with a saved payment method. + +When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card. + +So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one. + +In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition: + +```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" +if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) { + setSubmitting(false) + return +} +``` + +This allows the customer to place their order if they have selected a saved payment method but have not entered a new card. + +Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following: + +```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights} +await stripe +.confirmCardPayment(session?.data.client_secret as string, { + payment_method: session?.data.payment_method_id as string || { + card: card!, + billing_details: { + name: + cart.billing_address?.first_name + + " " + + cart.billing_address?.last_name, + address: { + city: cart.billing_address?.city ?? undefined, + country: cart.billing_address?.country_code ?? undefined, + line1: cart.billing_address?.address_1 ?? undefined, + line2: cart.billing_address?.address_2 ?? undefined, + postal_code: cart.billing_address?.postal_code ?? undefined, + state: cart.billing_address?.province ?? undefined, + }, + email: cart.email, + phone: cart.billing_address?.phone ?? undefined, + }, + }, +}) +.then(({ error, paymentIntent }) => { + if (error) { + const pi = error.payment_intent + + if ( + (pi && pi.status === "requires_capture") || + (pi && pi.status === "succeeded") + ) { + onPaymentCompleted() + } + + setErrorMessage(error.message || null) + return + } + + if ( + (paymentIntent && paymentIntent.status === "requires_capture") || + paymentIntent.status === "succeeded" + ) { + return onPaymentCompleted() + } + + return +}) +``` + +In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one. + +### Test it Out + +You can now test out placing orders with a saved payment method. + +To do that, start the Medusa application by running the following command in the Medusa application's directory: + +```bash npm2yarn +npm run dev +``` + +Then, start the Next.js Starter Storefront by running the following command in the storefront's directory: + +```bash npm2yarn +npm run dev +``` + +In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart. + +Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order. + +![Saved payment method in checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1745308047/Medusa%20Resources/Screenshot_2025-04-21_at_4.07.34_PM_krudjq.png) + +Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment. + +![Order in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745318042/Medusa%20Resources/Screenshot_2025-04-22_at_1.33.39_PM_uynlfp.png) + +*** + +## Next Steps + +You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders. + +You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method. + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # Implement Loyalty Points System in Medusa In this tutorial, you'll learn how to implement a loyalty points system in Medusa. @@ -45216,850 +46060,6 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). -# Use Saved Payment Methods During Checkout - -In this tutorial, you'll learn how to allow customers to save their payment methods and use them for future purchases. - -When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. The Medusa application's commerce features are built around [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md) which are available out-of-the-box. - -Medusa's architecture facilitates integrating third-party services, such as payment providers. These payment providers can process payments and securely store customers' payment methods for future use. - -In this tutorial, you'll expand on Medusa's [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) to allow customers to re-use their saved payment methods during checkout. - -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. - -While this tutorial uses Stripe as an example, you can follow the same steps to implement saved payment methods with other payment providers. - -## Summary - -By following this tutorial, you'll learn how to: - -- Install and set up Medusa and the Next.js Starter Storefront. -- Set up the Stripe Module Provider in Medusa. -- Customize the checkout flow to save customers' payment methods. -- Allow customers to select saved payment methods during checkout. - -![Diagram illustrating the features of this guide](https://res.cloudinary.com/dza7lstvk/image/upload/v1745309355/Medusa%20Resources/saved-payment-methods_orjnix.jpg) - -[Saved Payment Methods Repository](https://github.com/medusajs/examples/tree/main/stripe-saved-payment): Find the full code for this guide in this repository. - -*** - -## Step 1: Install a Medusa Application - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) - -Start by installing the Medusa application on your machine with the following command: - -```bash -npx create-medusa-app@latest -``` - -You'll first be asked for the project's name. Then, when asked whether you want to install the [Next.js starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md), choose Yes. - -Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a separate directory with the `{project-name}-storefront` name. - -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more in [Medusa's Architecture documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterwards, you can log in with the new user and explore the dashboard. - -Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. - -*** - -## Step 2: Set Up the Stripe Module Provider - -Medusa's [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) provides payment-related models and the interface to manage and process payments. However, it delegates the actual payment processing to module providers that integrate third-party payment services. - -The [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe/index.html.md) is a Payment Module Provider that integrates Stripe into your Medusa application to process payments. It can also save payment methods securely. - -In this section, you'll set up the Stripe Module Provider in your Medusa application. - -### Prerequisites - -- [Stripe account](https://stripe.com/) -- [Stripe Secret and Public API Keys](https://support.stripe.com/questions/locate-api-keys-in-the-dashboard) - -### Register the Stripe Module Provider - -To register the Stripe Module Provider in your Medusa application, add it to the array of providers passed to the Payment Module in `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - apiKey: process.env.STRIPE_API_KEY, - }, - }, - ], - }, - }, - ], -}) -``` - -The Medusa configuration accepts a `modules` array, which contains the modules to be loaded. While the Payment Module is loaded by default, you need to add it again when registering a new provider. - -You register provides in the `providers` option of the Payment Module. Each provider is an object with the following properties: - -- `resolve`: The package name of the provider. -- `id`: The ID of the provider. This is used to identify the provider in the Medusa application. -- `options`: The options to be passed to the provider. In this case, the `apiKey` option is required for the Stripe Module Provider. - -Learn about other options in the [Stripe Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-provider/stripe#module-options/index.html.md) documentation. - -### Add Environment Variables - -Next, add the following environment variables to your `.env` file: - -```plain -STRIPE_API_KEY=sk_... -``` - -Where `STRIPE_API_KEY` is your Stripe Secret API Key. You can find it in the Stripe dashboard under Developers > API keys. - -![Secret API Key in the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745313823/Medusa%20Resources/Screenshot_2025-04-22_at_12.20.08_PM_w6rxxo.png) - -### Enable Stripe in a Region - -In Medusa, each [region](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md) (which is a geographical area where your store operates) can have different payment methods enabled. So, after registering the Stripe Module Provider, you need to enable it in a region. - -To enable it in a region, start the Medusa application with the following command: - -```bash npm2yarn -npm run dev -``` - -Then, go to `localhost:9000/app` and log in with the user you created earlier. - -Once you're logged in: - -1. Go to Settings -> Regions. -2. Click on the region where you want to enable the payment provider. -3. Click the icon at the top right of the first section -4. Choose "Edit" from the dropdown menu -5. In the side window that opens, find the "Payment Providers" field and select Stripe from the dropdown. -6. Once you're done, click the "Save" button. - -Stripe will now be available as a payment option during checkout. - -The Stripe Module Provider supports different payment methods in Stripe, such as Bancontact or iDEAL. This guide focuses only on the card payment method, but you can enable other payment methods as well. - -![Stripe in the dropdown](https://res.cloudinary.com/dza7lstvk/image/upload/v1745245433/Medusa%20Resources/Screenshot_2025-04-21_at_5.23.07_PM_yd7xji.png) - -### Add Evnironement Variable to Storefront - -The [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) supports payment with Stripe during checkout if it's enabled in the region. - -The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. - -So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory: - -```bash -cd ../medusa-payment-storefront # change based on your project name -``` - -In the Next.js Starter Storefront project, add the Stripe public API key as an environment variable in `.env.local`: - -```plain badgeLabel="Storefront" badgeColor="blue" -NEXT_PUBLIC_STRIPE_KEY=pk_123... -``` - -Where `NEXT_PUBLIC_STRIPE_KEY` is your Stripe public API key. You can find it in the Stripe dashboard under Developers > API keys. - -*** - -## Step 3: List Payment Methods API Route - -The Payment Module uses [account holders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/account-holder/index.html.md) to represent a customer's details that are stored in a third-party payment provider. Medusa creates an account holder for each customer, allowing you later to retrieve the customer's saved payment methods in the third-party provider. - -![Diagram illustrating the relation between customers and account holders in Medusa, and customers in Stripe](https://res.cloudinary.com/dza7lstvk/image/upload/v1745314344/Medusa%20Resources/customer-account-stripe_in5ei6.jpg) - -While this feature is available out-of-the-box, you need to expose it to clients, like storefronts, by creating an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). An API Route is an endpoint that exposes commerce features to external applications and clients. - -In this step, you'll create an API route that lists the saved payment methods for an authenticated customer. - -Refer to the [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) documentation to learn more. - -### Create API Route - -An API route is created in a `route.ts` file under a sub-directory of the `src/api` directory. The path of the API route is the file's path relative to `src/api`, and it can include path parameters using square brackets. - -So, to create an API route at the path `/store/payment-methods/:account-holder-id`, create the file `src/api/store/payment-methods/[account_holder_id]/route.ts` with the following content: - -```ts title="src/api/store/payment-methods/[account_holder_id]/route.ts" highlights={apiRouteHighlights} -import { MedusaError } from "@medusajs/framework/utils" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { account_holder_id } = req.params - const query = req.scope.resolve("query") - const paymentModuleService = req.scope.resolve("payment") - - const { data: [accountHolder] } = await query.graph({ - entity: "account_holder", - fields: [ - "data", - "provider_id", - ], - filters: { - id: account_holder_id, - }, - }) - - if (!accountHolder) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Account holder not found" - ) - } - - const paymentMethods = await paymentModuleService.listPaymentMethods( - { - provider_id: accountHolder.provider_id, - context: { - account_holder: { - data: { - id: accountHolder.data.id, - }, - }, - }, - } - ) - - res.json({ - payment_methods: paymentMethods, - }) -} -``` - -Since you export a route handler function named `GET`, you expose a `GET` API route at `/store/payment-methods/:account-holder-id`. The route handler function accepts two parameters: - -1. A request object with details and context on the request, such as body parameters or authenticated customer details. -2. A response object to manipulate and send the response. - -The request object has a `scope` property, which is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). The Medusa container is a registry of Framework and commerce tools that you can access in the API route. - -You use the Medusa container to resolve: - -- [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), which is a tool that retrieves data across modules in the Medusa application. -- The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md)'s service, which provides an interface to manage and process payments with third-party providers. - -You use Query to retrieve the account holder with the ID passed as a path parameter. If the account holder is not found, you throw an error. - -Then, you use the [listPaymentMethods](https://docs.medusajs.com/references/payment/listPaymentMethods/index.html.md) method of the Payment Module's service to retrieve the payment providers saved in the third-party provider. The method accepts an object with the following properties: - -- `provider_id`: The ID of the provider, such as Stripe's ID. The account holder stores the ID its associated provider. -- `context`: The context of the request. In this case, you pass the account holder's ID to retrieve the payment methods associated with it in the third-party provider. - -Finally, you return the payment methods in the response. - -### Protect API Route - -Only authenticated customers can access and use saved payment methods. So, you need to protect the API route to ensure that only authenticated customers can access it. - -To protect an API route, you can add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). A middleware is a function executed when a request is sent to an API Route. You can add an authentication middleware that ensures that the request is authenticated before executing the route handler function. - -Refer to the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation to learn more. - -Middlewares are added in the `src/api/middlewares.ts` file. So, create the file with the following content: - -```ts title="src/api/middlewares.ts" -import { authenticate, defineMiddlewares } from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/store/payment-methods/:provider_id/:account_holder_id", - method: "GET", - middlewares: [ - authenticate("customer", ["bearer", "session"]), - ], - }, - ], -}) -``` - -The `src/api/middlewares.ts` file must use the `defineMiddlewares` function and export its result. The `defineMiddlewares` function accepts a `routes` array that accepts objects with the following properties: - -- `matcher`: The path of the API route to apply the middleware to. -- `method`: The HTTP method of the API route to apply the middleware to. -- `middlewares`: An array of middlewares to apply to the API route. - -You apply the `authenticate` middleware to the API route you created earlier. The `authenticate` middleware ensures that only authenticated customers can access the API route. - -Refer to the [Protected Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md) documentation to learn more about the `authenticate` middleware. - -Your API route can now only be accessed by authenticated customers. You'll test it out as you customize the Next.js Starter Storefront in the next steps. - -*** - -## Step 4: Save Payment Methods During Checkout - -In this step, you'll customize the checkout flow in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to save payment methods during checkout. - -The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. - -So, if your Medusa application's directory is `medusa-payment`, you can find the storefront by going back to the parent directory and changing to the `medusa-payment-storefront` directory: - -```bash -cd ../medusa-payment-storefront # change based on your project name -``` - -During checkout, when the customer chooses a payment method, such as Stripe, the Next.js Starter Storefront creates a [payment session](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/payment-session/index.html.md) in Medusa using the [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route. - -Under the hood, Medusa uses the associated payment provider (Stripe) to initiate the payment process with the associated third-party payment provider. The [Initialize Payment Session](https://docs.medusajs.com/api/store#payment-collections_postpaymentcollectionsidpaymentsessions) API route accepts a `data` object parameter in the request body that allows you to pass data relevant to the third-party payment provider. - -So, to save the payment method that the customer uses during checkout with Stripe, you must pass the `setup_future_usage` property in the `data` object. The `setup_future_usage` property is a Stripe-specific property that allows you to save the payment method for future use. - -In `src/modules/checkout/components/payment/index.tsx` of the Next.js Starter Storefront, there are two uses of the `initiatePaymentSession` function. Update each of them to pass the `data` property: - -```ts title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" -// update in two places -await initiatePaymentSession(cart, { - // ... - data: { - setup_future_usage: "off_session", - }, -}) -``` - -You customize the `initiatePaymentSession` function to pass the `data` object with the `setup_future_usage` property. You set the value to `off_session` to allow using the payment method outside of the checkout flow, such as for follow up payments. You can use `on_session` instead if you only want the payment method to be used by the customer during checkout. - -By making this change, you always save the payment method that the customer uses during checkout. You can alternatively show a checkbox to confirm saving the payment method, and only pass the `data` object if the customer checks it. - -### Test it Out - -To test it out, start the Medusa application by running the following command in the Medusa application's directory: - -```bash npm2yarn -npm run dev -``` - -Then, start the Next.js Starter Storefront by running the following command in the storefront's directory: - -```bash npm2yarn -npm run dev -``` - -You can open the storefront in your browser at `localhost:8000`. Then, create a new customer account by clicking on the "Account" link at the top right. - -After creating an account and logging in, add a product to the cart and go to the checkout page. Once you get to the payment step, choose Stripe and enter a [test card number](https://docs.stripe.com/testing#cards), such as `4242 4242 4242 4242`. - -Then, place the order. Once the order is placed, you can check on the Stripe dashboard that the payment method was saved by: - -1. Going to the "Customers" section in the Stripe dashboard. -2. Clicking on the customer you just placed the order with. -3. Scrolling down to the "Payment methods" section. You'll find the payment method you just used to place the order. - -![Saved payment method on the Stripe dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745249064/Medusa%20Resources/Screenshot_2025-04-21_at_6.24.00_PM_peotrd.png) - -In the next step, you'll show the saved payment methods to the customer during checkout and allow them to select one of them. - -*** - -## Step 5: Use Saved Payment Methods During Checkout - -In this step, you'll customize the checkout flow in the Next.js Starter Storefront to show the saved payment methods to the customer and allow them to select one of them to place the order. - -### Retrieve Saved Payment Methods - -To retrieve the saved payment methods, you'll add a server function that retrieves the customer's saved payment methods from the API route you created earlier. - -Add the following in `src/lib/data/payment.ts`: - -```ts title="src/lib/data/payment.ts" badgeLabel="Storefront" badgeColor="blue" highlights={paymentHighlights} -export type SavedPaymentMethod = { - id: string - provider_id: string - data: { - card: { - brand: string - last4: string - exp_month: number - exp_year: number - } - } -} - -export const getSavedPaymentMethods = async (accountHolderId: string) => { - const headers = { - ...(await getAuthHeaders()), - } - - return sdk.client.fetch<{ - payment_methods: SavedPaymentMethod[] - }>( - `/store/payment-methods/${accountHolderId}`, - { - method: "GET", - headers, - } - ).catch(() => { - return { - payment_methods: [], - } - }) -} -``` - -You define a type for the retrieved payment methods. It contains the following properties: - -- `id`: The ID of the payment method in the third-party provider. -- `provider_id`: The ID of the provider in the Medusa application, such as Stripe's ID. -- `data`: Additional data retrieved from the third-party provider related to the saved payment method. The type is modeled after the data returned by Stripe, but you can change it to match other payment providers. - -You also create a `getSavedPaymentMethods` function that retrieves the saved payment methods from the API route you created earlier. The function accepts the account holder ID as a parameter and returns the saved payment methods. - -### Add Saved Payment Methods Component - -Next, you'll add the component that shows the saved payment methods and allows the customer to select one of them. - -The component that shows the Stripe card element is defined in `src/modules/checkout/components/payment-container/index.tsx`. So, you'll define the component for the saved payment methods in the same file. - -Start by adding the following import statements at the top of the file: - -```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import { Button } from "@medusajs/ui" -import { useEffect, useState } from "react" -import { HttpTypes } from "@medusajs/types" -import { SavedPaymentMethod, getSavedPaymentMethods } from "@lib/data/payment" -import { initiatePaymentSession } from "../../../../lib/data/cart" -import { capitalize } from "lodash" -``` - -Then, update the `PaymentContainerProps` type to include the payment session and cart details: - -```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -type PaymentContainerProps = { - // ... - paymentSession?: HttpTypes.StorePaymentSession - cart: HttpTypes.StoreCart -} -``` - -You'll need these details to find which saved payment method the customer selected, and to initiate a new payment session for the cart when the customer chooses a saved payment method. - -Next, add the following component at the end of the file: - -```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeSavedPaymentMethodsHighlights} -const StripeSavedPaymentMethodsContainer = ({ - paymentSession, - setCardComplete, - setCardBrand, - setError, - cart, -}: { - paymentSession?: HttpTypes.StorePaymentSession - setCardComplete: (complete: boolean) => void - setCardBrand: (brand: string) => void - setError: (error: string | null) => void - cart: HttpTypes.StoreCart -}) => { - const [savedPaymentMethods, setSavedPaymentMethods] = useState< - SavedPaymentMethod[] - >([]) - const [selectedPaymentMethod, setSelectedPaymentMethod] = useState< - string | undefined - >( - paymentSession?.data?.payment_method_id as string | undefined - ) - - useEffect(() => { - const accountHolderId = ( - paymentSession?.context?.account_holder as Record - ) - ?.id - - if (!accountHolderId) { - return - } - - getSavedPaymentMethods(accountHolderId) - .then(({ payment_methods }) => { - setSavedPaymentMethods(payment_methods) - }) - }, [paymentSession]) - - useEffect(() => { - if (!selectedPaymentMethod || !savedPaymentMethods.length) { - setCardComplete(false) - setCardBrand("") - setError(null) - return - } - const selectedMethod = savedPaymentMethods.find( - (method) => method.id === selectedPaymentMethod - ) - - if (!selectedMethod) { - return - } - - setCardBrand(capitalize(selectedMethod.data.card.brand)) - setCardComplete(true) - setError(null) - }, [selectedPaymentMethod, savedPaymentMethods]) - - const handleSelect = async (method: SavedPaymentMethod) => { - // initiate a new payment session with the selected payment method - await initiatePaymentSession(cart, { - provider_id: method.provider_id, - data: { - payment_method_id: method.id, - }, - }).catch((error) => { - setError(error.message) - }) - - setSelectedPaymentMethod(method.id) - } - - if (!savedPaymentMethods.length) { - return <> - } - - // TODO add return statement -} -``` - -You define a `StripeSavedPaymentMethodsContainer` component that accepts the following props: - -- `paymentSession`: The cart's current payment session. -- `setCardComplete`: A function to tell parent components whether the cart or payment method selection is complete. This allows the customer to proceed to the next step in the checkout flow. -- `setCardBrand`: A function to set the brand of the selected payment method. This is useful to show the brand of the selected payment method in review sections of the checkout flow. -- `setError`: A function to set the error message in case of an error. -- `cart`: The cart's details. - -In the component, you define a state variable to store the saved payment methods and another one to store the selected payment method. - -Then, you use the `useEffect` hook to retrieve the saved payment methods for the account holder set in the cart's payment session. You use the `getSavedPaymentMethods` function you created earlier to retrieve the saved payment methods. - -You also use another `useEffect` hook that is executed when the selected payment method changes. In this hook, you check if the selected payment method is valid and set the card brand and completion status accordingly. - -Finally, you define a `handleSelect` function that you'll execute when the customer selects a saved payment method. It creates a new payment session with the selected payment method. - -To show the saved payment methods, replace the `TODO` with the following `return` statement: - -```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -return ( -
- - Choose a saved payment method: - - {savedPaymentMethods.map((method) => ( -
handleSelect(method)} - > -
- { - if (e.target.checked) { - handleSelect(method) - } - }} - /> -
- - {capitalize(method.data.card.brand)} •••• {method.data.card.last4} - - - Expires {method.data.card.exp_month}/{method.data.card.exp_year} - -
-
-
- ))} -
-) -``` - -You display the saved payment methods as radio buttons. When the customer selects one of them, you execute the `handleSelect` function to initiate a new payment session with the selected payment method. - -### Modify Existing Stripe Element - -Now that you have the component to show the saved payment methods, you need to modify the existing Stripe element to allow customers to select an existing payment method or enter a new one. - -In the same `src/modules/checkout/components/payment-container/index.tsx` file, expand the new `paymentSession` and `cart` props of the `StripeCardContainer` component: - -```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -export const StripeCardContainer = ({ - // ... - paymentSession, - cart, -}: Omit & { - // ... -}) => { - // ... -} -``` - -Then, add a new state variable that keeps track of whether the customer is using a saved payment method or entering a new one: - -```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const [isUsingSavedPaymentMethod, setIsUsingSavedPaymentMethod] = useState( - paymentSession?.data?.payment_method_id !== undefined -) -``` - -Next, add a function that resets the payment session when the customer switches between saved and new payment methods: - -```ts title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" -const handleRefreshSession = async () => { - await initiatePaymentSession(cart, { - provider_id: paymentProviderId, - }) - setIsUsingSavedPaymentMethod(false) -} -``` - -This function initiates a new payment session for the cart and disables the `isUsingSavedPaymentMethod` state variable. - -Finally, replace the `return` statement of the `StripeCardContainer` component with the following: - -```tsx title="src/modules/checkout/components/payment-container/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={stripeCardReturnHighlights} -return ( - - {selectedPaymentOptionId === paymentProviderId && - (stripeReady ? ( -
- - {isUsingSavedPaymentMethod && ( - - )} - {!isUsingSavedPaymentMethod && ( - <> - - Enter your card details: - - { - setCardBrand( - e.brand && e.brand.charAt(0).toUpperCase() + e.brand.slice(1) - ) - setError(e.error?.message || null) - setCardComplete(e.complete) - }} - /> - - )} -
- ) : ( - - ))} -
-) -``` - -You update the `return` statement to: - -- Pass the new `paymentSession` and `cart` props to the `PaymentContainer` component. -- Show the `StripeSavedPaymentMethodsContainer` component before Stripe's card element. -- Add a button that's shown when the customer selects a saved payment method. The button allows the customer to switch back to entering a new payment method. - -The existing Stripe element in checkout will now show the saved payment methods to the customer along with the component to enter a card's details. - -Since you added new props to the `StripeCardContainer` and `PaymentContainer` components, you need to update other components that use them to pass the props. - -In `src/modules/checkout/components/payment/index.tsx`, find usages of `StripeCardContainer` and `PaymentContainer` in the return statement and add the `paymentSession` and `cart` props: - -```tsx title="src/modules/checkout/components/payment/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["5"], ["6"], ["11"], ["12"]]} -
- {isStripeFunc(paymentMethod.id) ? ( - - ) : ( - - )} -
-``` - -### Support Updating Stripe's Client Secret - -The Next.js Starter Storefront uses Stripe's `Elements` component to wrap the payment elements. The `Elements` component requires a `clientSecret` prop, which is available in the cart's payment session. - -With the recent changes, the client secret will be updated whenever a payment session is initiated, such as when the customer selects a saved payment method. However, the `options.clientSecret` prop of the `Elements` component is immutable, meaning that it cannot be changed after the component is mounted. - -To force the component to re-mount and update the `clientSecret` prop, you can add a `key` prop to the `Elements` component. The `key` prop ensures that the `Elements` component re-mounts whenever the client secret changes, allowing Stripe to process the updated payment session. - -In `src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx`, find the `Elements` component in the `return` statement and add the `key` prop: - -```tsx title="src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"]]} - - {children} - -``` - -You set the `key` prop to the client secret, which forces the `Elements` component to re-mount whenever the client secret changes. - -### Support Payment with Saved Payment Method - -The last change you need to make ensures that the customer can place an order with a saved payment method. - -When the customer places the order, and they've chosen Stripe as a payment method, the Next.js Starter Storefront uses Stripe's `confirmCardPayment` method to confirm the payment. This method accepts either the ID of a saved payment method, or the details of a new card. - -So, you need to update the `confirmCardPayment` usage to support passing the ID of the selected payment method if the customer has selected one. - -In `src/modules/checkout/components/payment-button/index.tsx`, find the `handlePayment` method and update its first `if` condition: - -```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" -if (!stripe || !elements || (!card && !session?.data.payment_method_id) || !cart) { - setSubmitting(false) - return -} -``` - -This allows the customer to place their order if they have selected a saved payment method but have not entered a new card. - -Then, find the usage of `confirmCardPayment` in the `handlePayment` function and change it to the following: - -```ts title="src/modules/checkout/components/payment-button/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={confirmPaymentHighlights} -await stripe -.confirmCardPayment(session?.data.client_secret as string, { - payment_method: session?.data.payment_method_id as string || { - card: card!, - billing_details: { - name: - cart.billing_address?.first_name + - " " + - cart.billing_address?.last_name, - address: { - city: cart.billing_address?.city ?? undefined, - country: cart.billing_address?.country_code ?? undefined, - line1: cart.billing_address?.address_1 ?? undefined, - line2: cart.billing_address?.address_2 ?? undefined, - postal_code: cart.billing_address?.postal_code ?? undefined, - state: cart.billing_address?.province ?? undefined, - }, - email: cart.email, - phone: cart.billing_address?.phone ?? undefined, - }, - }, -}) -.then(({ error, paymentIntent }) => { - if (error) { - const pi = error.payment_intent - - if ( - (pi && pi.status === "requires_capture") || - (pi && pi.status === "succeeded") - ) { - onPaymentCompleted() - } - - setErrorMessage(error.message || null) - return - } - - if ( - (paymentIntent && paymentIntent.status === "requires_capture") || - paymentIntent.status === "succeeded" - ) { - return onPaymentCompleted() - } - - return -}) -``` - -In particular, you're changing the `payment_method` property to either be the ID of the selected payment method, or the details of a new card. This allows the customer to place an order with either a saved payment method or a new one. - -### Test it Out - -You can now test out placing orders with a saved payment method. - -To do that, start the Medusa application by running the following command in the Medusa application's directory: - -```bash npm2yarn -npm run dev -``` - -Then, start the Next.js Starter Storefront by running the following command in the storefront's directory: - -```bash npm2yarn -npm run dev -``` - -In the Next.js Starter Storefront, login with the customer account you created earlier and add a product to the cart. - -Then, proceed to the checkout flow. In the payment step, you should see the saved payment method you used earlier. You can select it and place the order. - -![Saved payment method in checkout](https://res.cloudinary.com/dza7lstvk/image/upload/v1745308047/Medusa%20Resources/Screenshot_2025-04-21_at_4.07.34_PM_krudjq.png) - -Once the order is placed successfully, you can check it in the Medusa Admin dashboard. You can view the order and capture the payment. - -![Order in the Medusa Admin dashboard](https://res.cloudinary.com/dza7lstvk/image/upload/v1745318042/Medusa%20Resources/Screenshot_2025-04-22_at_1.33.39_PM_uynlfp.png) - -*** - -## Next Steps - -You've added support for saved payment methods in your Medusa application and Next.js Starter Storefront, allowing customers to save their payment methods during checkout and use them in future orders. - -You can add more features to the saved payment methods, such as allowing customers to delete saved payment methods. You can use [Stripe's APIs](https://docs.stripe.com/api/payment_methods/detach) in the storefront or add an [API route](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) in Medusa to delete the saved payment method. - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # Implement Product Reviews in Medusa In this tutorial, you'll learn how to implement product reviews in Medusa. @@ -49232,6 +49232,1054 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# How to Build Magento Data Migration Plugin + +In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa. + +Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery. + +Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business. + +By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects. + +## Summary + +### Prerequisites + + + +This tutorial will teach you how to: + +- Install and set up a Medusa application project. +- Install and set up a Medusa plugin. +- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products. + - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more. +- Trigger data migration from Magento to Medusa in a scheduled job. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg) + +[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard. + +*** + +## Step 1: Install a Medusa Application + +You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out. + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll be asked for the project's name. You can also optionally choose to install the [Next.js starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). + +Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more. + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Install a Medusa Plugin Project + +A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application. + +Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins. + +A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application. + +To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application: + +```bash npm2yarn +npx create-medusa-app@latest medusa-plugin-magento --plugin +``` + +Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`. + +Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files. + +![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) + +*** + +## Step 3: Set up Plugin in Medusa Application + +Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process. + +In the plugin's directory, run the following command to publish the plugin to the local package registry: + +```bash title="Plugin project" +npx medusa plugin:publish +``` + +This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. + +Next, you'll install the plugin in the Medusa application from the local registry. + +If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first. + +Run the following command in the Medusa application's directory to install the plugin: + +```bash title="Medusa application" +npx medusa plugin:add medusa-plugin-magento +``` + +This command installs the plugin in the Medusa application from the local package registry. + +Next, register the plugin in the `medusa-config.ts` file of the Medusa application: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "medusa-plugin-magento", + options: { + // TODO add options + }, + }, + ], +}) +``` + +You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento. + +Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development: + +```bash title="Plugin project" +npx medusa plugin:develop +``` + +*** + +## Step 4: Implement Magento Module + +To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products. + +Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules. + +### Create Module Directory + +A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`. + +![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg) + +### Create Module's Service + +You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database. + +In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data. + +Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content: + +![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg) + +```ts title="src/modules/magento/service.ts" +type Options = { + baseUrl: string + storeCode?: string + username: string + password: string + migrationOptions?: { + imageBaseUrl?: string + } +} + +export default class MagentoModuleService { + private options: Options + + constructor({}, options: Options) { + this.options = { + ...options, + storeCode: options.storeCode || "default", + } + } +} +``` + +You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include: + +- `baseUrl`: The base URL of the Magento server. +- `storeCode`: The store code of the Magento store, which is `default` by default. +- `username`: The username of a Magento admin user to authenticate with the Magento server. +- `password`: The password of the Magento admin user. +- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images. + +The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options. + +### Add Authentication Logic + +To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server. + +First, add the following property to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + private accessToken: { + token: string + expiresAt: Date + } + // ... +} +``` + +You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token. + +Next, add the following `authenticate` method to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +import { MedusaError } from "@medusajs/framework/utils" + +export default class MagentoModuleService { + // ... + async authenticate() { + const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: this.options.username, password: this.options.password }), + }) + + const token = await response.text() + + if (!response.ok) { + throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`) + } + + this.accessToken = { + token: token.replaceAll("\"", ""), + expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds + } + } +} +``` + +You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body. + +If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento. + +Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async isAccessTokenExpired(): Promise { + return !this.accessToken || this.accessToken.expiresAt < new Date() + } +} +``` + +In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token. + +### Retrieve Products from Magento + +Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size. + +First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content: + +![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg) + +```ts title="src/modules/magento/types.ts" +export type MagentoProduct = { + id: number + sku: string + name: string + price: number + status: number + // not handling other types + type_id: "simple" | "configurable" + created_at: string + updated_at: string + extension_attributes: { + category_links: { + category_id: string + }[] + configurable_product_links?: number[] + configurable_product_options?: { + id: number + attribute_id: string + label: string + position: number + values: { + value_index: number + }[] + }[] + } + media_gallery_entries: { + id: number + media_type: string + label: string + position: number + disabled: boolean + types: string[] + file: string + }[] + custom_attributes: { + attribute_code: string + value: string + }[] + // added by module + children?: MagentoProduct[] +} + +export type MagentoAttribute = { + attribute_code: string + attribute_id: number + default_frontend_label: string + options: { + label: string + value: string + }[] +} + +export type MagentoPagination = { + search_criteria: { + filter_groups: [], + page_size: number + current_page: number + } + total_count: number +} + +export type MagentoPaginatedResponse = { + items: TData[] +} & MagentoPagination +``` + +You define the following types: + +- `MagentoProduct`: Represents a product in Magento. +- `MagentoAttribute`: Represents an attribute in Magento. +- `MagentoPagination`: Represents the pagination information returned by Magento's API. +- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products. + +Next, add the `getProducts` method to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async getProducts(options?: { + currentPage?: number + pageSize?: number + }): Promise<{ + products: MagentoProduct[] + attributes: MagentoAttribute[] + pagination: MagentoPagination + }> { + const { currentPage = 1, pageSize = 100 } = options || {} + const getAccessToken = await this.isAccessTokenExpired() + if (getAccessToken) { + await this.authenticate() + } + + // TODO prepare query params + } +} +``` + +The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. + +Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const searchQuery = new URLSearchParams() +// pass pagination parameters +searchQuery.append( + "searchCriteria[currentPage]", + currentPage?.toString() || "1" +) +searchQuery.append( + "searchCriteria[pageSize]", + pageSize?.toString() || "100" +) + +// retrieve only configurable products +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][field]", + "type_id" +) +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][value]", + "configurable" +) +searchQuery.append( + "searchCriteria[filter_groups][1][filters][0][condition_type]", + "in" +) + +// TODO send request to retrieve products +``` + +You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products. + +Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } +).then((res) => res.json()) +.catch((err) => { + console.log(err) + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get products from Magento: ${err.message}` + ) +}) + +// TODO prepare products +``` + +You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. + +Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const attributeIds: string[] = [] + +await promiseAll( + products.map(async (product) => { + // retrieve its children + product.children = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } + ).then((res) => res.json()) + .catch((err) => { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get product children from Magento: ${err.message}` + ) + }) + + product.media_gallery_entries = product.media_gallery_entries.map( + (entry) => ({ + ...entry, + file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`, + } + )) + + attributeIds.push(...( + product.extension_attributes.configurable_product_options?.map( + (option) => option.attribute_id) || [] + ) + ) + }) +) + +// TODO retrieve attributes +``` + +You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided. + +In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes. + +Add the new method `getAttributes` to the `MagentoModuleService` class: + +```ts title="src/modules/magento/service.ts" +export default class MagentoModuleService { + // ... + async getAttributes({ + ids, + }: { + ids: string[] + }): Promise { + const getAccessToken = await this.isAccessTokenExpired() + if (getAccessToken) { + await this.authenticate() + } + + // filter by attribute IDs + const searchQuery = new URLSearchParams() + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][field]", + "attribute_id" + ) + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][value]", + ids.join(",") + ) + searchQuery.append( + "searchCriteria[filter_groups][0][filters][0][condition_type]", + "in" + ) + + const { + items: attributes, + }: MagentoPaginatedResponse = await fetch( + `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, + { + headers: { + "Authorization": `Bearer ${this.accessToken.token}`, + }, + } + ).then((res) => res.json()) + .catch((err) => { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Failed to get attributes from Magento: ${err.message}` + ) + }) + + return attributes + } +} +``` + +The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. + +Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. + +Finally, you return the retrieved attributes. + +Now, go back to the `getProducts` method and replace the `TODO` with the following: + +```ts title="src/modules/magento/service.ts" +const attributes = await this.getAttributes({ ids: attributeIds }) + +return { products, attributes, pagination } +``` + +You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information. + +You'll use this method in a later step to retrieve products from Magento. + +### Export Module Definition + +The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. + +So, create the file `src/modules/magento/index.ts` with the following content: + +![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg) + +```ts title="src/modules/magento/index.ts" +import { Module } from "@medusajs/framework/utils" +import MagentoModuleService from "./service" + +export const MAGENTO_MODULE = "magento" + +export default Module(MAGENTO_MODULE, { + service: MagentoModuleService, +}) +``` + +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: + +1. The module's name, which is `magento`. +2. An object with a required property `service` indicating the module's service. + +You'll later use the module's service to retrieve products from Magento. + +### Pass Options to Plugin + +As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin. + +So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + plugins: [ + { + resolve: "medusa-plugin-magento", + options: { + baseUrl: process.env.MAGENTO_BASE_URL, + username: process.env.MAGENTO_USERNAME, + password: process.env.MAGENTO_PASSWORD, + migrationOptions: { + imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL, + }, + }, + }, + ], +}) +``` + +You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file: + +```bash +MAGENTO_BASE_URL=https://magento.example.com +MAGENTO_USERNAME=admin +MAGENTO_PASSWORD=password +MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product +``` + +Where: + +- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`. +- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server. +- `MAGENTO_PASSWORD`: The password of the Magento admin user. +- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option. + +Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider. + +You can now use the Magento Module to migrate data, which you'll do in the next steps. + +*** + +## Step 5: Build Product Migration Workflow + +In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow. + +A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job. + +By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs. + +Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows. + +### Workflow Steps + +The workflow you'll create will have the following steps: + +- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them. +- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application. +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application. + +You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package. + +### getMagentoProductsStep + +The first step of the workflow retrieves and returns the products from Magento. + +In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content: + +![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg) + +```ts title="src/workflows/steps/get-magento-products.ts" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { MAGENTO_MODULE } from "../../modules/magento" +import MagentoModuleService from "../../modules/magento/service" + +type GetMagentoProductsInput = { + currentPage: number + pageSize: number +} + +export const getMagentoProductsStep = createStep( + "get-magento-products", + async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => { + const magentoModuleService: MagentoModuleService = + container.resolve(MAGENTO_MODULE) + + const response = await magentoModuleService.getProducts({ + currentPage, + pageSize, + }) + + return new StepResponse(response) + } +) +``` + +You create a step using `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's name, which is `get-magento-products`. +2. An async function that executes the step's logic. The function receives two parameters: + - The input data for the step, which in this case is the pagination parameters. + - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools. + +In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento. + +Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return. + +### Create migrateProductsFromMagentoWorkflow + +You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package. + +In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content: + +![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg) + +```ts title="src/workflows/migrate-products-from-magento.ts" +import { + createWorkflow, transform, WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + CreateProductWorkflowInputDTO, UpsertProductDTO, +} from "@medusajs/framework/types" +import { + createProductsWorkflow, + updateProductsWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" +import { getMagentoProductsStep } from "./steps/get-magento-products" + +type MigrateProductsFromMagentoWorkflowInput = { + currentPage: number + pageSize: number +} + +export const migrateProductsFromMagentoWorkflowId = + "migrate-products-from-magento" + +export const migrateProductsFromMagentoWorkflow = createWorkflow( + { + name: migrateProductsFromMagentoWorkflowId, + retentionTime: 10000, + store: true, + }, + (input: MigrateProductsFromMagentoWorkflowInput) => { + const { pagination, products, attributes } = getMagentoProductsStep( + input + ) + // TODO prepare data to create and update products + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters: + +1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard. +2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. + +In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input. + +Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update. + +Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +const { data: stores } = useQueryGraphStep({ + entity: "store", + fields: ["supported_currencies.*", "default_sales_channel_id"], + pagination: { + take: 1, + skip: 0, + }, +}) + +const { data: shippingProfiles } = useQueryGraphStep({ + entity: "shipping_profile", + fields: ["id"], + pagination: { + take: 1, + skip: 0, + }, +}).config({ name: "get-shipping-profiles" }) + +// TODO retrieve existing products +``` + +You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules. + +Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. + +Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +const externalIdFilters = transform({ + products, +}, (data) => { + return data.products.map((product) => product.id.toString()) +}) + +const { data: existingProducts } = useQueryGraphStep({ + entity: "product", + fields: ["id", "external_id", "variants.id", "variants.metadata"], + filters: { + external_id: externalIdFilters, + }, +}).config({ name: "get-existing-products" }) + +// TODO prepare products to create or update +``` + +Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation. + +Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints. + +Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters: + +- The data to transform, which in this case is the Magento products. +- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data. + +In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento. + +Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights} +const { + productsToCreate, + productsToUpdate, +} = transform({ + products, + attributes, + stores, + shippingProfiles, + existingProducts, +}, (data) => { + const productsToCreate = new Map() + const productsToUpdate = new Map() + + data.products.forEach((magentoProduct) => { + const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = { + title: magentoProduct.name, + description: magentoProduct.custom_attributes.find( + (attr) => attr.attribute_code === "description" + )?.value, + status: magentoProduct.status === 1 ? "published" : "draft", + handle: magentoProduct.custom_attributes.find( + (attr) => attr.attribute_code === "url_key" + )?.value, + external_id: magentoProduct.id.toString(), + thumbnail: magentoProduct.media_gallery_entries.find( + (entry) => entry.types.includes("thumbnail") + )?.file, + sales_channels: [{ + id: data.stores[0].default_sales_channel_id, + }], + shipping_profile_id: data.shippingProfiles[0].id, + } + const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id) + + if (existingProduct) { + productData.id = existingProduct.id + } + + productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => { + const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id)) + return { + title: option.label, + values: attribute?.options.filter((opt) => { + return option.values.find((v) => v.value_index === parseInt(opt.value)) + }).map((opt) => opt.label) || [], + } + }) || [] + + productData.variants = magentoProduct.children?.map((child) => { + const childOptions: Record = {} + + child.custom_attributes.forEach((attr) => { + const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code) + if (!attrData) { + return + } + + childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || "" + }) + + const variantExternalId = child.id.toString() + const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId) + + return { + title: child.name, + sku: child.sku, + options: childOptions, + prices: data.stores[0].supported_currencies.map(({ currency_code }) => { + return { + amount: child.price, + currency_code, + } + }), + metadata: { + external_id: variantExternalId, + }, + id: existingVariant?.id, + } + }) + + productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => { + return { + url: entry.file, + metadata: { + external_id: entry.id.toString(), + }, + } + }) + + if (productData.id) { + productsToUpdate.set(existingProduct.id, productData) + } else { + productsToCreate.set(productData.external_id!, productData) + } + }) + + return { + productsToCreate: Array.from(productsToCreate.values()), + productsToUpdate: Array.from(productsToUpdate.values()), + } +}) + +// TODO create and update products +``` + +You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data: + +- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes. +- You associate the product with the default sales channel and shipping profile retrieved previously. +- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento. +- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property. +- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property. + +In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants. + +Finally, you return the products to create and update. + +The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following: + +```ts title="src/workflows/migrate-products-from-magento.ts" +createProductsWorkflow.runAsStep({ + input: { + products: productsToCreate, + }, +}) + +updateProductsWorkflow.runAsStep({ + input: { + products: productsToUpdate, + }, +}) + +return new WorkflowResponse(pagination) +``` + +You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application. + +Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process. + +You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps. + +*** + +## Step 6: Schedule Product Migration + +There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. + +Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs. + +To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content: + +![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg) + +```ts title="src/jobs/migrate-magento.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { migrateProductsFromMagentoWorkflow } from "../workflows" + +export default async function migrateMagentoJob( + container: MedusaContainer +) { + const logger = container.resolve("logger") + logger.info("Migrating products from Magento...") + + let currentPage = 0 + const pageSize = 100 + let totalCount = 0 + + do { + currentPage++ + + const { + result: pagination, + } = await migrateProductsFromMagentoWorkflow(container).run({ + input: { + currentPage, + pageSize, + }, + }) + + totalCount = pagination.total_count + } while (currentPage * pageSize < totalCount) + + logger.info("Finished migrating products from Magento") +} + +export const config = { + name: "migrate-magento-job", + schedule: "0 0 * * *", +} +``` + +A scheduled job file must export: + +- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. +- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string. + +In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate. + +Based on the job's configurations, the Medusa application will run the job at midnight every day. + +### Test it Out + +To test out this scheduled job, first, change the configuration to run the job every minute: + +```ts title="src/jobs/migrate-magento.ts" +export const config = { + // ... + schedule: "* * * * *", +} +``` + +Then, make sure to run the `plugin:develop` command in the plugin if you haven't already: + +```bash +npx medusa plugin:develop +``` + +This ensures that the plugin's latest changes are reflected in the Medusa application. + +Finally, start the Medusa application that the plugin is installed in: + +```bash npm2yarn +npm run dev +``` + +After a minute, you'll see a message in the terminal indicating that the migration started: + +```plain title="Terminal" +info: Migrating products from Magento... +``` + +Once the migration is done, you'll see the following message: + +```plain title="Terminal" +info: Finished migrating products from Magento +``` + +To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products. + +![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png) + +*** + +## Next Steps + +You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to: + +- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed. +- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin). + +If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. + +To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). + + # Implement Localization in Medusa by Integrating Contentful In this tutorial, you'll learn how to localize your Medusa store's data with Contentful. @@ -52001,1054 +53049,6 @@ If you encounter issues not covered in the troubleshooting guides: 3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. -# How to Build Magento Data Migration Plugin - -In this tutorial, you'll learn how to build a [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) that migrates data, such as products, from Magento to Medusa. - -Magento is known for its customization capabilities. However, its monolithic architecture imposes limitations on business requirements, often forcing development teams to implement hacky workarounds. Over time, these customizations become challenging to maintain, especially as the business scales, leading to increased technical debt and slower feature delivery. - -Medusa's modular architecture allows you to build a custom digital commerce platform that meets your business requirements without the limitations of a monolithic system. By migrating from Magento to Medusa, you can take advantage of Medusa's modern technology stack to build a scalable and flexible commerce platform that grows with your business. - -By following this tutorial, you'll create a Medusa plugin that migrates data from a Magento server to a Medusa application in minimal time. You can re-use this plugin across multiple Medusa applications, allowing you to adopt Medusa across your projects. - -## Summary - -### Prerequisites - - - -This tutorial will teach you how to: - -- Install and set up a Medusa application project. -- Install and set up a Medusa plugin. -- Implement a Magento Module in the plugin to connect to Magento's APIs and retrieve products. - - This guide will only focus on migrating product data from Magento to Medusa. You can extend the implementation to migrate other data, such as customers, orders, and more. -- Trigger data migration from Magento to Medusa in a scheduled job. - -You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. - -![Diagram showcasing the flow of migrating data from Magento to Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1739360550/Medusa%20Resources/magento-summary_hsewci.jpg) - -[Example Repository](https://github.com/medusajs/examples/tree/main/migrate-from-magento): Find the full code of the guide in this repository. The repository also includes additional features, such as triggering migrations from the Medusa Admin dashboard. - -*** - -## Step 1: Install a Medusa Application - -You'll first install a Medusa application that exposes core commerce features through REST APIs. You'll later install the Magento plugin in this application to test it out. - -### Prerequisites - -- [Node.js v20+](https://nodejs.org/en/download) -- [Git CLI tool](https://git-scm.com/downloads) -- [PostgreSQL](https://www.postgresql.org/download/) - -Start by installing the Medusa application on your machine with the following command: - -```bash -npx create-medusa-app@latest -``` - -You'll be asked for the project's name. You can also optionally choose to install the [Next.js starter storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). - -Afterward, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the `{project-name}-storefront` name. - -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Refer to the [Medusa Architecture](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md) documentation to learn more. - -Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credentials and submit the form. Afterward, you can log in with the new user and explore the dashboard. - -Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. - -*** - -## Step 2: Install a Medusa Plugin Project - -A plugin is a package of reusable Medusa customizations that you can install in any Medusa application. You can add in the plugin [API Routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md), [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), and other customizations, as you'll see in this guide. Afterward, you can test it out locally in a Medusa application, then publish it to npm to install and use it in any Medusa application. - -Refer to the [Plugins](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) documentation to learn more about plugins. - -A Medusa plugin is set up in a different project, giving you the flexibility in building and publishing it, while providing you with the tools to test it out locally in a Medusa application. - -To create a new Medusa plugin project, run the following command in a directory different than that of the Medusa application: - -```bash npm2yarn -npx create-medusa-app@latest medusa-plugin-magento --plugin -``` - -Where `medusa-plugin-magento` is the name of the plugin's directory and the name set in the plugin's `package.json`. So, if you wish to publish it to NPM later under a different name, you can change it here in the command or later in `package.json`. - -Once the installation process is done, a new directory named `medusa-plugin-magento` will be created with the plugin project files. - -![Directory structure of a plugin project](https://res.cloudinary.com/dza7lstvk/image/upload/v1737019441/Medusa%20Book/project-dir_q4xtri.jpg) - -*** - -## Step 3: Set up Plugin in Medusa Application - -Before you start your development, you'll set up the plugin in the Medusa application you installed in the first step. This will allow you to test the plugin during your development process. - -In the plugin's directory, run the following command to publish the plugin to the local package registry: - -```bash title="Plugin project" -npx medusa plugin:publish -``` - -This command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. The plugin is published locally under the name you specified in `package.json`. - -Next, you'll install the plugin in the Medusa application from the local registry. - -If you've installed your Medusa project before v2.3.1, you must install [yalc](https://github.com/wclr/yalc) as a development dependency first. - -Run the following command in the Medusa application's directory to install the plugin: - -```bash title="Medusa application" -npx medusa plugin:add medusa-plugin-magento -``` - -This command installs the plugin in the Medusa application from the local package registry. - -Next, register the plugin in the `medusa-config.ts` file of the Medusa application: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "medusa-plugin-magento", - options: { - // TODO add options - }, - }, - ], -}) -``` - -You add the plugin to the array of plugins. Later, you'll pass options useful to retrieve data from Magento. - -Finally, to ensure your plugin's changes are constantly published to the local registry, simplifying your testing process, keep the following command running in the plugin project during development: - -```bash title="Plugin project" -npx medusa plugin:develop -``` - -*** - -## Step 4: Implement Magento Module - -To connect to external applications in Medusa, you create a custom module. A module is a reusable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -In this step, you'll create a Magento Module in the Magento plugin that connects to a Magento server's REST APIs and retrieves data, such as products. - -Refer to the [Modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) documentation to learn more about modules. - -### Create Module Directory - -A module is created under the `src/modules` directory of your plugin. So, create the directory `src/modules/magento`. - -![Diagram showcasing the module directory to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272368/magento-1_ikev4x.jpg) - -### Create Module's Service - -You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to external systems or the database, which is useful if your module defines tables in the database. - -In this section, you'll create the Magento Module's service that connects to Magento's REST APIs and retrieves data. - -Start by creating the file `src/modules/magento/service.ts` in the plugin with the following content: - -![Diagram showcasing the service file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739272483/magento-2_ajetpr.jpg) - -```ts title="src/modules/magento/service.ts" -type Options = { - baseUrl: string - storeCode?: string - username: string - password: string - migrationOptions?: { - imageBaseUrl?: string - } -} - -export default class MagentoModuleService { - private options: Options - - constructor({}, options: Options) { - this.options = { - ...options, - storeCode: options.storeCode || "default", - } - } -} -``` - -You create a `MagentoModuleService` that has an `options` property to store the module's options. These options include: - -- `baseUrl`: The base URL of the Magento server. -- `storeCode`: The store code of the Magento store, which is `default` by default. -- `username`: The username of a Magento admin user to authenticate with the Magento server. -- `password`: The password of the Magento admin user. -- `migrationOptions`: Additional options useful for migrating data, such as the base URL to use for product images. - -The service's constructor accepts as a first parameter the [Module Container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which allows you to access resources available for the module. As a second parameter, it accepts the module's options. - -### Add Authentication Logic - -To authenticate with the Magento server, you'll add a method to the service that retrieves an access token from Magento using the username and password in the options. This access token is used in subsequent requests to the Magento server. - -First, add the following property to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - private accessToken: { - token: string - expiresAt: Date - } - // ... -} -``` - -You add an `accessToken` property to store the access token and its expiration date. The access token Magento returns expires after four hours, so you store the expiration date to know when to refresh the token. - -Next, add the following `authenticate` method to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -import { MedusaError } from "@medusajs/framework/utils" - -export default class MagentoModuleService { - // ... - async authenticate() { - const response = await fetch(`${this.options.baseUrl}/rest/${this.options.storeCode}/V1/integration/admin/token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ username: this.options.username, password: this.options.password }), - }) - - const token = await response.text() - - if (!response.ok) { - throw new MedusaError(MedusaError.Types.UNAUTHORIZED, `Failed to authenticate with Magento: ${token}`) - } - - this.accessToken = { - token: token.replaceAll("\"", ""), - expiresAt: new Date(Date.now() + 4 * 60 * 60 * 1000), // 4 hours in milliseconds - } - } -} -``` - -You create an `authenticate` method that sends a POST request to the Magento server's `/rest/{storeCode}/V1/integration/admin/token` endpoint, passing the username and password in the request body. - -If the request is successful, you store the access token and its expiration date in the `accessToken` property. If the request fails, you throw a `MedusaError` with the error message returned by Magento. - -Lastly, add an `isAccessTokenExpired` method that checks if the access token has expired: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async isAccessTokenExpired(): Promise { - return !this.accessToken || this.accessToken.expiresAt < new Date() - } -} -``` - -In the `isAccessTokenExpired` method, you return a boolean indicating whether the access token has expired. You'll use this in later methods to check if you need to refresh the access token. - -### Retrieve Products from Magento - -Next, you'll add a method that retrieves products from Magento. Due to limitations in Magento's API that makes it difficult to differentiate between simple products that don't belong to a configurable product and those that do, you'll only retrieve configurable products and their children. You'll also retrieve the configurable attributes of the product, such as color and size. - -First, you'll add some types to represent a Magento product and its attributes. Create the file `src/modules/magento/types.ts` in the plugin with the following content: - -![Diagram showcasing the types file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739346287/Medusa%20Resources/magento-3_fpghog.jpg) - -```ts title="src/modules/magento/types.ts" -export type MagentoProduct = { - id: number - sku: string - name: string - price: number - status: number - // not handling other types - type_id: "simple" | "configurable" - created_at: string - updated_at: string - extension_attributes: { - category_links: { - category_id: string - }[] - configurable_product_links?: number[] - configurable_product_options?: { - id: number - attribute_id: string - label: string - position: number - values: { - value_index: number - }[] - }[] - } - media_gallery_entries: { - id: number - media_type: string - label: string - position: number - disabled: boolean - types: string[] - file: string - }[] - custom_attributes: { - attribute_code: string - value: string - }[] - // added by module - children?: MagentoProduct[] -} - -export type MagentoAttribute = { - attribute_code: string - attribute_id: number - default_frontend_label: string - options: { - label: string - value: string - }[] -} - -export type MagentoPagination = { - search_criteria: { - filter_groups: [], - page_size: number - current_page: number - } - total_count: number -} - -export type MagentoPaginatedResponse = { - items: TData[] -} & MagentoPagination -``` - -You define the following types: - -- `MagentoProduct`: Represents a product in Magento. -- `MagentoAttribute`: Represents an attribute in Magento. -- `MagentoPagination`: Represents the pagination information returned by Magento's API. -- `MagentoPaginatedResponse`: Represents a paginated response from Magento's API for a specific item type, such as products. - -Next, add the `getProducts` method to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async getProducts(options?: { - currentPage?: number - pageSize?: number - }): Promise<{ - products: MagentoProduct[] - attributes: MagentoAttribute[] - pagination: MagentoPagination - }> { - const { currentPage = 1, pageSize = 100 } = options || {} - const getAccessToken = await this.isAccessTokenExpired() - if (getAccessToken) { - await this.authenticate() - } - - // TODO prepare query params - } -} -``` - -The `getProducts` method receives an optional `options` object with the `currentPage` and `pageSize` properties. So far, you check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. - -Next, you'll prepare the query parameters to pass in the request that retrieves products. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const searchQuery = new URLSearchParams() -// pass pagination parameters -searchQuery.append( - "searchCriteria[currentPage]", - currentPage?.toString() || "1" -) -searchQuery.append( - "searchCriteria[pageSize]", - pageSize?.toString() || "100" -) - -// retrieve only configurable products -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][field]", - "type_id" -) -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][value]", - "configurable" -) -searchQuery.append( - "searchCriteria[filter_groups][1][filters][0][condition_type]", - "in" -) - -// TODO send request to retrieve products -``` - -You create a `searchQuery` object to store the query parameters to pass in the request. Then, you add the pagination parameters and the filter to retrieve only configurable products. - -Next, you'll send the request to retrieve products from Magento. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const { items: products, ...pagination }: MagentoPaginatedResponse = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products?${searchQuery}`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } -).then((res) => res.json()) -.catch((err) => { - console.log(err) - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get products from Magento: ${err.message}` - ) -}) - -// TODO prepare products -``` - -You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. - -Next, you'll prepare the retrieved products by retrieving their children, configurable attributes, and modifying their image URLs. Replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const attributeIds: string[] = [] - -await promiseAll( - products.map(async (product) => { - // retrieve its children - product.children = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/configurable-products/${product.sku}/children`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } - ).then((res) => res.json()) - .catch((err) => { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get product children from Magento: ${err.message}` - ) - }) - - product.media_gallery_entries = product.media_gallery_entries.map( - (entry) => ({ - ...entry, - file: `${this.options.migrationOptions?.imageBaseUrl}${entry.file}`, - } - )) - - attributeIds.push(...( - product.extension_attributes.configurable_product_options?.map( - (option) => option.attribute_id) || [] - ) - ) - }) -) - -// TODO retrieve attributes -``` - -You loop over the retrieved products and retrieve their children using the `/rest/{storeCode}/V1/configurable-products/{sku}/children` endpoint. You also modify the image URLs to use the base URL in the migration options, if provided. - -In addition, you store the IDs of the configurable products' attributes in the `attributeIds` array. You'll add a method that retrieves these attributes. - -Add the new method `getAttributes` to the `MagentoModuleService` class: - -```ts title="src/modules/magento/service.ts" -export default class MagentoModuleService { - // ... - async getAttributes({ - ids, - }: { - ids: string[] - }): Promise { - const getAccessToken = await this.isAccessTokenExpired() - if (getAccessToken) { - await this.authenticate() - } - - // filter by attribute IDs - const searchQuery = new URLSearchParams() - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][field]", - "attribute_id" - ) - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][value]", - ids.join(",") - ) - searchQuery.append( - "searchCriteria[filter_groups][0][filters][0][condition_type]", - "in" - ) - - const { - items: attributes, - }: MagentoPaginatedResponse = await fetch( - `${this.options.baseUrl}/rest/${this.options.storeCode}/V1/products/attributes?${searchQuery}`, - { - headers: { - "Authorization": `Bearer ${this.accessToken.token}`, - }, - } - ).then((res) => res.json()) - .catch((err) => { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Failed to get attributes from Magento: ${err.message}` - ) - }) - - return attributes - } -} -``` - -The `getAttributes` method receives an object with the `ids` property, which is an array of attribute IDs. You check if the access token has expired and, if so, retrieve a new one using the `authenticate` method. - -Next, you prepare the query parameters to pass in the request to retrieve attributes. You send a `GET` request to the Magento server's `/rest/{storeCode}/V1/products/attributes` endpoint, passing the query parameters in the URL. You also pass the access token in the `Authorization` header. - -Finally, you return the retrieved attributes. - -Now, go back to the `getProducts` method and replace the `TODO` with the following: - -```ts title="src/modules/magento/service.ts" -const attributes = await this.getAttributes({ ids: attributeIds }) - -return { products, attributes, pagination } -``` - -You retrieve the configurable products' attributes using the `getAttributes` method and return the products, attributes, and pagination information. - -You'll use this method in a later step to retrieve products from Magento. - -### Export Module Definition - -The final piece to a module is its definition, which you export in an `index.ts` file at its root directory. This definition tells Medusa the name of the module and its service. - -So, create the file `src/modules/magento/index.ts` with the following content: - -![Diagram showcasing the module definition file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739348316/Medusa%20Resources/magento-4_bmepvh.jpg) - -```ts title="src/modules/magento/index.ts" -import { Module } from "@medusajs/framework/utils" -import MagentoModuleService from "./service" - -export const MAGENTO_MODULE = "magento" - -export default Module(MAGENTO_MODULE, { - service: MagentoModuleService, -}) -``` - -You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: - -1. The module's name, which is `magento`. -2. An object with a required property `service` indicating the module's service. - -You'll later use the module's service to retrieve products from Magento. - -### Pass Options to Plugin - -As mentioned earlier when you registered the plugin in the Medusa Application's `medusa-config.ts` file, you can pass options to the plugin. These options are then passed to the modules in the plugin. - -So, add the following options to the plugin's registration in the `medusa-config.ts` file of the Medusa application: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - plugins: [ - { - resolve: "medusa-plugin-magento", - options: { - baseUrl: process.env.MAGENTO_BASE_URL, - username: process.env.MAGENTO_USERNAME, - password: process.env.MAGENTO_PASSWORD, - migrationOptions: { - imageBaseUrl: process.env.MAGENTO_IMAGE_BASE_URL, - }, - }, - }, - ], -}) -``` - -You pass the options that you defined in the `MagentoModuleService`. Make sure to also set their environment variables in the `.env` file: - -```bash -MAGENTO_BASE_URL=https://magento.example.com -MAGENTO_USERNAME=admin -MAGENTO_PASSWORD=password -MAGENTO_IMAGE_BASE_URL=https://magento.example.com/pub/media/catalog/product -``` - -Where: - -- `MAGENTO_BASE_URL`: The base URL of the Magento server. It can also be a local URL, such as `http://localhost:8080`. -- `MAGENTO_USERNAME`: The username of a Magento admin user to authenticate with the Magento server. -- `MAGENTO_PASSWORD`: The password of the Magento admin user. -- `MAGENTO_IMAGE_BASE_URL`: The base URL to use for product images. Magento stores product images in the `pub/media/catalog/product` directory, so you can reference them directly or use a CDN URL. If the URLs of product images in the Medusa server already have a different base URL, you can omit this option. - -Medusa supports integrating third-party services, such as [S3](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/s3/index.html.md), in a File Module Provider. Refer to the [File Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/file/index.html.md) documentation to find other module providers and how to create a custom provider. - -You can now use the Magento Module to migrate data, which you'll do in the next steps. - -*** - -## Step 5: Build Product Migration Workflow - -In this section, you'll add the feature to migrate products from Magento to Medusa. To implement this feature, you'll use a workflow. - -A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in an API route or a scheduled job. - -By implementing the migration feature in a workflow, you ensure that the data remains consistent and that the migration process can be rolled back if an error occurs. - -Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) documentation to learn more about workflows. - -### Workflow Steps - -The workflow you'll create will have the following steps: - -- [getMagentoProductsStep](#getMagentoProductsStep): Retrieve products from Magento using the Magento Module. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Medusa store details, which you'll need when creating the products. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve a shipping profile, which you'll associate the created products with. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve Magento products that are already in Medusa to update them, instead of creating them. -- [createProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductsWorkflow/index.html.md): Create products in the Medusa application. -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md): Update existing products in the Medusa application. - -You only need to implement the `getMagentoProductsStep` step, which retrieves the products from Magento. The other steps and workflows are provided by Medusa's `@medusajs/medusa/core-flows` package. - -### getMagentoProductsStep - -The first step of the workflow retrieves and returns the products from Magento. - -In your plugin, create the file `src/workflows/steps/get-magento-products.ts` with the following content: - -![Diagram showcasing the get-magento-products file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349590/Medusa%20Resources/magento-5_ueb4wn.jpg) - -```ts title="src/workflows/steps/get-magento-products.ts" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { MAGENTO_MODULE } from "../../modules/magento" -import MagentoModuleService from "../../modules/magento/service" - -type GetMagentoProductsInput = { - currentPage: number - pageSize: number -} - -export const getMagentoProductsStep = createStep( - "get-magento-products", - async ({ currentPage, pageSize }: GetMagentoProductsInput, { container }) => { - const magentoModuleService: MagentoModuleService = - container.resolve(MAGENTO_MODULE) - - const response = await magentoModuleService.getProducts({ - currentPage, - pageSize, - }) - - return new StepResponse(response) - } -) -``` - -You create a step using `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's name, which is `get-magento-products`. -2. An async function that executes the step's logic. The function receives two parameters: - - The input data for the step, which in this case is the pagination parameters. - - An object holding the workflow's context, including the [Medusa Container](https://docs.medusajs.com/docslearn/fundamentals/medusa-container/index.html.md) that allows you to resolve Framework and commerce tools. - -In the step function, you resolve the Magento Module's service from the container, then use its `getProducts` method to retrieve the products from Magento. - -Steps that return data must return them in a `StepResponse` instance. The `StepResponse` constructor accepts as a parameter the data to return. - -### Create migrateProductsFromMagentoWorkflow - -You'll now create the workflow that migrates products from Magento using the step you created and steps from Medusa's `@medusajs/medusa/core-flows` package. - -In your plugin, create the file `src/workflows/migrate-products-from-magento.ts` with the following content: - -![Diagram showcasing the migrate-products-from-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739349820/Medusa%20Resources/magento-6_jjdaxj.jpg) - -```ts title="src/workflows/migrate-products-from-magento.ts" -import { - createWorkflow, transform, WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - CreateProductWorkflowInputDTO, UpsertProductDTO, -} from "@medusajs/framework/types" -import { - createProductsWorkflow, - updateProductsWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" -import { getMagentoProductsStep } from "./steps/get-magento-products" - -type MigrateProductsFromMagentoWorkflowInput = { - currentPage: number - pageSize: number -} - -export const migrateProductsFromMagentoWorkflowId = - "migrate-products-from-magento" - -export const migrateProductsFromMagentoWorkflow = createWorkflow( - { - name: migrateProductsFromMagentoWorkflowId, - retentionTime: 10000, - store: true, - }, - (input: MigrateProductsFromMagentoWorkflowInput) => { - const { pagination, products, attributes } = getMagentoProductsStep( - input - ) - // TODO prepare data to create and update products - } -) -``` - -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts two parameters: - -1. An object with the workflow's configuration, including the name and whether to store the workflow's executions. You enable storing the workflow execution so that you can view it later in the Medusa Admin dashboard. -2. A worflow constructor function, which holds the workflow's implementation. The function receives the input data for the workflow, which is the pagination parameters. - -In the workflow constructor function, you use the `getMagentoProductsStep` step to retrieve the products from Magento, passing it the pagination parameters from the workflow's input. - -Next, you'll retrieve the Medusa store details and shipping profiles. These are necessary to prepare the data of the products to create or update. - -Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -const { data: stores } = useQueryGraphStep({ - entity: "store", - fields: ["supported_currencies.*", "default_sales_channel_id"], - pagination: { - take: 1, - skip: 0, - }, -}) - -const { data: shippingProfiles } = useQueryGraphStep({ - entity: "shipping_profile", - fields: ["id"], - pagination: { - take: 1, - skip: 0, - }, -}).config({ name: "get-shipping-profiles" }) - -// TODO retrieve existing products -``` - -You use the `useQueryGraphStep` step to retrieve the store details and shipping profiles. `useQueryGraphStep` is a Medusa step that wraps [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), allowing you to use it in a workflow. Query is a tool that retrieves data across modules. - -Whe retrieving the store details, you specifically retrieve its supported currencies and default sales channel ID. You'll associate the products with the store's default sales channel, and set their variant prices in the supported currencies. You'll also associate the products with a shipping profile. - -Next, you'll retrieve products that were previously migrated from Magento to determine which products to create or update. Replace the `TODO` with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -const externalIdFilters = transform({ - products, -}, (data) => { - return data.products.map((product) => product.id.toString()) -}) - -const { data: existingProducts } = useQueryGraphStep({ - entity: "product", - fields: ["id", "external_id", "variants.id", "variants.metadata"], - filters: { - external_id: externalIdFilters, - }, -}).config({ name: "get-existing-products" }) - -// TODO prepare products to create or update -``` - -Since the Medusa application creates an internal representation of the workflow's constructor function, you can't manipulate data directly, as variables have no value while creating the internal representation. - -Refer to the [Workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md) documentation to learn more about the workflow constructor function's constraints. - -Instead, you can manipulate data in a workflow's constructor function using `transform` from the Workflows SDK. `transform` is a function that accepts two parameters: - -- The data to transform, which in this case is the Magento products. -- A function that transforms the data. The function receives the data passed in the first parameter and returns the transformed data. - -In the transformation function, you return the IDs of the Magento products. Then, you use the `useQueryGraphStep` to retrieve products in the Medusa application that have an `external_id` property matching the IDs of the Magento products. You'll use this property to store the IDs of the products in Magento. - -Next, you'll prepare the data to create and update the products. Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" highlights={prepareHighlights} -const { - productsToCreate, - productsToUpdate, -} = transform({ - products, - attributes, - stores, - shippingProfiles, - existingProducts, -}, (data) => { - const productsToCreate = new Map() - const productsToUpdate = new Map() - - data.products.forEach((magentoProduct) => { - const productData: CreateProductWorkflowInputDTO | UpsertProductDTO = { - title: magentoProduct.name, - description: magentoProduct.custom_attributes.find( - (attr) => attr.attribute_code === "description" - )?.value, - status: magentoProduct.status === 1 ? "published" : "draft", - handle: magentoProduct.custom_attributes.find( - (attr) => attr.attribute_code === "url_key" - )?.value, - external_id: magentoProduct.id.toString(), - thumbnail: magentoProduct.media_gallery_entries.find( - (entry) => entry.types.includes("thumbnail") - )?.file, - sales_channels: [{ - id: data.stores[0].default_sales_channel_id, - }], - shipping_profile_id: data.shippingProfiles[0].id, - } - const existingProduct = data.existingProducts.find((p) => p.external_id === productData.external_id) - - if (existingProduct) { - productData.id = existingProduct.id - } - - productData.options = magentoProduct.extension_attributes.configurable_product_options?.map((option) => { - const attribute = data.attributes.find((attr) => attr.attribute_id === parseInt(option.attribute_id)) - return { - title: option.label, - values: attribute?.options.filter((opt) => { - return option.values.find((v) => v.value_index === parseInt(opt.value)) - }).map((opt) => opt.label) || [], - } - }) || [] - - productData.variants = magentoProduct.children?.map((child) => { - const childOptions: Record = {} - - child.custom_attributes.forEach((attr) => { - const attrData = data.attributes.find((a) => a.attribute_code === attr.attribute_code) - if (!attrData) { - return - } - - childOptions[attrData.default_frontend_label] = attrData.options.find((opt) => opt.value === attr.value)?.label || "" - }) - - const variantExternalId = child.id.toString() - const existingVariant = existingProduct.variants.find((v) => v.metadata.external_id === variantExternalId) - - return { - title: child.name, - sku: child.sku, - options: childOptions, - prices: data.stores[0].supported_currencies.map(({ currency_code }) => { - return { - amount: child.price, - currency_code, - } - }), - metadata: { - external_id: variantExternalId, - }, - id: existingVariant?.id, - } - }) - - productData.images = magentoProduct.media_gallery_entries.filter((entry) => !entry.types.includes("thumbnail")).map((entry) => { - return { - url: entry.file, - metadata: { - external_id: entry.id.toString(), - }, - } - }) - - if (productData.id) { - productsToUpdate.set(existingProduct.id, productData) - } else { - productsToCreate.set(productData.external_id!, productData) - } - }) - - return { - productsToCreate: Array.from(productsToCreate.values()), - productsToUpdate: Array.from(productsToUpdate.values()), - } -}) - -// TODO create and update products -``` - -You use `transform` again to prepare the data to create and update the products in the Medusa application. For each Magento product, you map its equivalent Medusa product's data: - -- You set the product's general details, such as the title, description, status, handle, external ID, and thumbnail using the Magento product's data and custom attributes. -- You associate the product with the default sales channel and shipping profile retrieved previously. -- You map the Magento product's configurable product options to Medusa product options. In Medusa, a product's option has a label, such as "Color", and values, such as "Red". To map the option values, you use the attributes retrieved from Magento. -- You map the Magento product's children to Medusa product variants. For the variant options, you pass an object whose keys is the option's label, such as "Color", and values is the option's value, such as "Red". For the prices, you set the variant's price based on the Magento child's price for every supported currency in the Medusa store. Also, you set the Magento child product's ID in the Medusa variant's `metadata.external_id` property. -- You map the Magento product's media gallery entries to Medusa product images. You filter out the thumbnail image and set the URL and the Magento image's ID in the Medusa image's `metadata.external_id` property. - -In addition, you use the existing products retrieved in the previous step to determine whether a product should be created or updated. If there's an existing product whose `external_id` matches the ID of the magento product, you set the existing product's ID in the `id` property of the product to be updated. You also do the same for its variants. - -Finally, you return the products to create and update. - -The last steps of the workflow is to create and update the products. Replace the `TODO` in the workflow function with the following: - -```ts title="src/workflows/migrate-products-from-magento.ts" -createProductsWorkflow.runAsStep({ - input: { - products: productsToCreate, - }, -}) - -updateProductsWorkflow.runAsStep({ - input: { - products: productsToUpdate, - }, -}) - -return new WorkflowResponse(pagination) -``` - -You use the `createProductsWorkflow` and `updateProductsWorkflow` workflows from Medusa's `@medusajs/medusa/core-flows` package to create and update the products in the Medusa application. - -Workflows must return an instance of `WorkflowResponse`, passing as a parameter the data to return to the workflow's executor. This workflow returns the pagination parameters, allowing you to paginate the product migration process. - -You can now use this workflow to migrate products from Magento to Medusa. You'll learn how to use it in the next steps. - -*** - -## Step 6: Schedule Product Migration - -There are many ways to execute tasks asynchronously in Medusa, such as [scheduling a job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) or [handling emitted events](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -In this guide, you'll learn how to schedule the product migration at a specified interval using a scheduled job. A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. - -Refer to the [Scheduled Jobs](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md) documentation to learn more about scheduled jobs. - -To create a scheduled job, in your plugin, create the file `src/jobs/migrate-magento.ts` with the following content: - -![Diagram showcasing the migrate-magento file to create](https://res.cloudinary.com/dza7lstvk/image/upload/v1739358924/Medusa%20Resources/magento-7_rqoodo.jpg) - -```ts title="src/jobs/migrate-magento.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { migrateProductsFromMagentoWorkflow } from "../workflows" - -export default async function migrateMagentoJob( - container: MedusaContainer -) { - const logger = container.resolve("logger") - logger.info("Migrating products from Magento...") - - let currentPage = 0 - const pageSize = 100 - let totalCount = 0 - - do { - currentPage++ - - const { - result: pagination, - } = await migrateProductsFromMagentoWorkflow(container).run({ - input: { - currentPage, - pageSize, - }, - }) - - totalCount = pagination.total_count - } while (currentPage * pageSize < totalCount) - - logger.info("Finished migrating products from Magento") -} - -export const config = { - name: "migrate-magento-job", - schedule: "0 0 * * *", -} -``` - -A scheduled job file must export: - -- An asynchronous function that executes the job's logic. The function receives the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) as a parameter. -- An object with the job's configuration, including the name and the schedule. The schedule is a cron job pattern as a string. - -In the job function, you resolve the [logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the container to log messages. Then, you paginate the product migration process by running the `migrateProductsFromMagentoWorkflow` workflow at each page until you've migrated all products. You use the pagination result returned by the workflow to determine whether there are more products to migrate. - -Based on the job's configurations, the Medusa application will run the job at midnight every day. - -### Test it Out - -To test out this scheduled job, first, change the configuration to run the job every minute: - -```ts title="src/jobs/migrate-magento.ts" -export const config = { - // ... - schedule: "* * * * *", -} -``` - -Then, make sure to run the `plugin:develop` command in the plugin if you haven't already: - -```bash -npx medusa plugin:develop -``` - -This ensures that the plugin's latest changes are reflected in the Medusa application. - -Finally, start the Medusa application that the plugin is installed in: - -```bash npm2yarn -npm run dev -``` - -After a minute, you'll see a message in the terminal indicating that the migration started: - -```plain title="Terminal" -info: Migrating products from Magento... -``` - -Once the migration is done, you'll see the following message: - -```plain title="Terminal" -info: Finished migrating products from Magento -``` - -To confirm that the products were migrated, open the Medusa Admin dashboard at `http://localhost:9000/app` and log in. Then, click on Products in the sidebar. You'll see your magento products in the list of products. - -![Click on products at the sidebar on the right, then view the products in the table in the middle.](https://res.cloudinary.com/dza7lstvk/image/upload/v1739359394/Medusa%20Resources/Screenshot_2025-02-12_at_1.22.44_PM_uva98i.png) - -*** - -## Next Steps - -You've now implemented the logic to migrate products from Magento to Medusa. You can re-use the plugin across Medusa applications. You can also expand on the plugin to: - -- Migrate other entities, such as orders, customers, and categories. Migrating other entities follows the same pattern as migrating products, using workflows and scheduled jobs. You only need to format the data to be migrated as needed. -- Allow triggering migrations from the Medusa Admin dashboard using [Admin Customizations](https://docs.medusajs.com/docs/learn/fundamentals/admin/index.html.md). This feature is available in the [Example Repository](https://github.com/medusajs/example-repository/tree/main/src/admin). - -If you're new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you'll get a more in-depth learning of all the concepts you've used in this guide and more. - -To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). - - # Integrate Medusa with Resend (Email Notifications) In this guide, you'll learn how to integrate Medusa with Resend. @@ -59209,353 +59209,353 @@ To learn more about the commerce features that Medusa provides, check out Medusa ## JS SDK Admin -- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.create/index.html.md) +- [batchSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.batchSalesChannels/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.list/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.update/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) - [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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.list/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/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) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) +- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/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) +- [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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) -- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) -- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.retrieve/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.list/index.html.md) -- [clearToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken_/index.html.md) +- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.list/index.html.md) - [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/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) - [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) - [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/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) - [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) - [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) -- [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) -- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) +- [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) - [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) +- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) +- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) +- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/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) +- [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/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) - [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/index.html.md) - [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md) - [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md) -- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md) -- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) -- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md) - [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md) +- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) +- [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/index.html.md) - [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) +- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) -- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) - [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md) +- [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md) - [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md) - [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) - [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.update/index.html.md) - [updateActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionShippingMethod/index.html.md) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/index.html.md) - [updateShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateShippingMethod/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/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) -- [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) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteInboundShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.list/index.html.md) -- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeItem/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeInboundItem/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) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.request/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundShipping/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) -- [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) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/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) -- [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) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) -- [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) -- [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/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) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateItem/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) - [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) - [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/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) -- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/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) -- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) -- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) +- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/index.html.md) +- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundShipping/index.html.md) +- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundItems/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) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/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) +- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) +- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) +- [listFulfillmentOptions](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentProvider/methods/js_sdk.admin.FulfillmentProvider.listFulfillmentOptions/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.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) - [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) - [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/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) -- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.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) -- [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) - [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) - [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) - [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) -- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.update/index.html.md) +- [listChanges](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listChanges/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrieve/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) -- [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/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) -- [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) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) -- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/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) +- [batchInventoryItemsLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemsLocationLevels/index.html.md) +- [batchInventoryItemLocationLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchInventoryItemLocationLevels/index.html.md) +- [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) +- [deleteLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.deleteLevel/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) +- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.retrieve/index.html.md) +- [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) - [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/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) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) +- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) +- [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/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) +- [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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/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) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.update/index.html.md) - [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/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) - [create](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.create/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) - [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md) -- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) - [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) -- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/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) +- [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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) - [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) - [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/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) -- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) - [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) - [updateVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateVariant/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/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) +- [updateOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.updateOption/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) -- [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/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/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) +- [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/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) -- [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/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) -- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/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) - [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) -- [listRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRules/index.html.md) +- [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.list/index.html.md) - [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/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) +- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/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) -- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.updateRules/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/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/Reservation/methods/js_sdk.admin.Reservation.delete/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) +- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/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) +- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) +- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/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) +- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) +- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/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) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/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) +- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) +- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) +- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) +- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.retrieve/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.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) - [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) -- [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) -- [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/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.update/index.html.md) -- [addReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnShipping/index.html.md) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) -- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) -- [confirmRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmRequest/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) -- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) -- [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) -- [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) -- [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.list/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) -- [removeDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeDismissItem/index.html.md) -- [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) -- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) -- [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) -- [updateReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnItem/index.html.md) -- [updateReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReturnShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/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/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) -- [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.delete/index.html.md) - [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.updateProducts/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/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) - [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) +- [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) +- [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) +- [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/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.retrieve/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/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/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) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/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) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/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) - [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/User/methods/js_sdk.admin.User.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) -- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) ## JS SDK Auth - [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) -- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) - [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/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) +- [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) - [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) ## JS SDK Store -- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) +- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/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) -- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/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) +- [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) +- [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) @@ -59579,632 +59579,6 @@ These layout components allow you to set the layout of your [UI routes](https:// These components allow you to use common Medusa Admin components in your custom UI routes and widgets. -# Single Column Layout - Admin Components - -The Medusa Admin has pages with a single column of content. - -This doesn't include the sidebar, only the main content. - -![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.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: - -```tsx title="src/admin/layouts/single-column.tsx" -export type SingleColumnLayoutProps = { - children: React.ReactNode -} - -export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { - return ( -
- {children} -
- ) -} -``` - -The `SingleColumnLayout` accepts the content in the `children` props. - -*** - -## Example - -Use the `SingleColumnLayout` 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 { SingleColumnLayout } from "../../layouts/single-column" -import { Header } from "../../components/header" - -const CustomPage = () => { - return ( - - -
- - - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -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. - - -# Two Column Layout - Admin Components - -The Medusa Admin has pages with two columns of content. - -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) - -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: - -```tsx title="src/admin/layouts/two-column.tsx" -export type TwoColumnLayoutProps = { - firstCol: React.ReactNode - secondCol: React.ReactNode -} - -export const TwoColumnLayout = ({ - firstCol, - secondCol, -}: TwoColumnLayoutProps) => { - return ( -
-
- {firstCol} -
-
- {secondCol} -
-
- ) -} -``` - -The `TwoColumnLayout` accepts two props: - -- `firstCol` indicating the content of the first column. -- `secondCol` indicating the content of the second column. - -*** - -## 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 ( - -
- - } - secondCol={ - -
- - } - /> - ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -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. - - -# Data Table - Admin Components - -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). - -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. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. - -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) - -You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. - -This guide focuses on how to use the `DataTable` component while fetching data from the backend. 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. - -## Example: DataTable with Data Fetching - -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. - -Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - createDataTableColumnHelper, -} from "@medusajs/ui" -import { - HttpTypes, -} from "@medusajs/framework/types" - -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"} - - ) - }, - }), -] -``` - -`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: - -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". - -Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. - -To define the filters, add the following: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - createDataTableFilterHelper, -} from "@medusajs/ui" - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] -``` - -`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. - -You'll now start creating the UI widget's component. Start by adding the necessary state variables: - -```tsx title="src/admin/routes/custom/page.tsx" -// other imports... -import { - // ... - DataTablePaginationState, - DataTableFilteringState, - DataTableSortingState, -} from "@medusajs/ui" -import { useMemo, useState } from "react" - -// ... - -const limit = 15 - -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]) - - // TODO add data fetching logic -} -``` - -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. - -You've also added two memoized variables: - -- `offset`: How many items to skip when fetching data based on the current page. -- `statusFilters`: The selected status filters, if any. - -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: - -```tsx title="src/admin/routes/custom/page.tsx" -import { sdk } from "../../lib/config" -import { useQuery } from "@tanstack/react-query" -``` - -This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). - -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]], -}) - -// TODO configure data table -``` - -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: - -- `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. - -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: - -```tsx title="src/admin/routes/custom/page.tsx" -import { - // ... - useDataTable, -} from "@medusajs/ui" -import { useNavigate } from "react-router-dom" -``` - -Then, replace the `TODO` in the component with the following: - -```tsx title="src/admin/routes/custom/page.tsx" -const navigate = useNavigate() - -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, - }, - onRowClick: (event, row) => { - // Handle row click, for example - navigate(`/products/${row.id}`) - }, -}) - -// TODO render component -``` - -The `useDataTable` hook accepts an object with the following properties: - -- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. -- data: (\`array\`) The products fetched from the Medusa application. -- getRowId: (\`function\`) A function that returns the unique ID of a row. -- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. -- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. -- pagination: (\`object\`) An object to configure pagination. - - - state: (\`object\`) The pagination React state variable. - - - onPaginationChange: (\`function\`) A function that updates the pagination state. -- search: (\`object\`) An object to configure searching. - - - state: (\`string\`) The search query React state variable. - - - onSearchChange: (\`function\`) A function that updates the search query state. -- filtering: (\`object\`) An object to configure filtering. - - - state: (\`object\`) The filtering React state variable. - - - onFilteringChange: (\`function\`) A function that updates the filtering state. -- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. -- sorting: (\`object\`) An object to configure sorting. - - - state: (\`object\`) The sorting React state variable. - - - onSortingChange: (\`function\`) A function that updates the sorting state. -- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. - - - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. - - - row: (\`object\`) The data of the row that was clicked. - -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" -import { - // ... - DataTable, -} from "@medusajs/ui" -import { SingleColumnLayout } from "../../layouts/single-column" -import { Container } from "../../components/container" -``` - -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: - -```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", - icon: ChatBubbleLeftRight, -}) - -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. - -### 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" - -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"} - - ) - }, - }), -] - -const filterHelper = createDataTableFilterHelper() - -const filters = [ - filterHelper.accessor("status", { - type: "select", - label: "Status", - options: [ - { - label: "Published", - value: "published", - }, - { - label: "Draft", - value: "draft", - }, - ], - }), -] - -const limit = 15 - -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, - }, - }) - - return ( - - - - - Products -
- - - -
-
- - -
-
-
- ) -} - -export const config = defineRouteConfig({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - - # Action Menu - Admin Components The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. @@ -60489,762 +59863,6 @@ 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. -# 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. - -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 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 `Header` 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 ( - -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -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 - -Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. - -![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) - -To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content: - -```tsx title="src/admin/components/json-view-section.tsx" -import { - ArrowUpRightOnBox, - Check, - SquareTwoStack, - TriangleDownMini, - XMarkMini, -} from "@medusajs/icons" -import { - Badge, - Container, - Drawer, - Heading, - IconButton, - Kbd, -} from "@medusajs/ui" -import Primitive from "@uiw/react-json-view" -import { CSSProperties, MouseEvent, Suspense, useState } from "react" - -type JsonViewSectionProps = { - data: object - title?: string -} - -export const JsonViewSection = ({ data }: JsonViewSectionProps) => { - const numberOfKeys = Object.keys(data).length - - return ( - -
- JSON - - {numberOfKeys} keys - -
- - - - - - - -
-
- - - - {numberOfKeys} - - - -
-
- - esc - - - - - - -
-
- -
-
} - > - - } /> - ( - null - )} - /> - ( - undefined - )} - /> - { - return ( - - {Object.keys(value as object).length} items - - ) - }} - /> - - - - - : - - { - return - }} - /> - - - -
-
-
-
- ) -} - -type CopiedProps = { - style?: CSSProperties - value: object | undefined -} - -const Copied = ({ style, value }: CopiedProps) => { - const [copied, setCopied] = useState(false) - - const handler = (e: MouseEvent) => { - e.stopPropagation() - setCopied(true) - - if (typeof value === "string") { - navigator.clipboard.writeText(value) - } else { - const json = JSON.stringify(value, null, 2) - navigator.clipboard.writeText(json) - } - - setTimeout(() => { - setCopied(false) - }, 2000) - } - - const styl = { whiteSpace: "nowrap", width: "20px" } - - if (copied) { - 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 `SectionRow` component shows a title and a value in the same row. - -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 `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 { Container } from "../components/container" -import { Header } from "../components/header" -import { SectionRow } from "../components/section-row" - -const ProductWidget = () => { - return ( - -
- - - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -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. - - -# Table - Admin Components - -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 as it provides features for sorting, filtering, pagination, and more with a simpler API. - -You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables. - -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/components/table.tsx" -import { useMemo } from "react" -import { Table as UiTable } from "@medusajs/ui" - -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 -} - -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) - } - } - - 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} - )} - - - ))} - - ) - })} - - - -
- ) -} -``` - -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. - -It accepts the following props: - -- columns: (\`object\[]\`) The table's columns. - - - key: (\`string\`) The column's key in the passed \`data\` - - - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. - - - 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. - -*** - -## Example - -Use the `Table` 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 { 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) - - return ( - - { - 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 -``` - -This widget also uses the [Container](../container.mdx) custom component. - -*** - -## Example With Data Fetching - -This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. - -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" - -const CustomPage = () => { - 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({ - label: "Custom", - icon: ChatBubbleLeftRight, -}) - -export default CustomPage -``` - -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. - - # Forms - Admin Components The Medusa Admin has two types of forms: @@ -61818,6 +60436,1388 @@ 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. +# Data Table - Admin Components + +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). + +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. It's used across the Medusa Admin dashboard to showcase a list of items, such as a list of products. + +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) + +You can use this component in your Admin Extensions to display data in a table format, especially if you're retrieving them from API routes of the Medusa application. + +This guide focuses on how to use the `DataTable` component while fetching data from the backend. 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. + +## Example: DataTable with Data Fetching + +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. + +Start by initializing the columns in the data table. To do that, use the `createDataTableColumnHelper` from Medusa UI: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + createDataTableColumnHelper, +} from "@medusajs/ui" +import { + HttpTypes, +} from "@medusajs/framework/types" + +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"} + + ) + }, + }), +] +``` + +`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: + +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". + +Next, you'll define the filters that can be applied to the data table. You'll configure filtering by product status. + +To define the filters, add the following: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + createDataTableFilterHelper, +} from "@medusajs/ui" + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] +``` + +`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. + +You'll now start creating the UI widget's component. Start by adding the necessary state variables: + +```tsx title="src/admin/routes/custom/page.tsx" +// other imports... +import { + // ... + DataTablePaginationState, + DataTableFilteringState, + DataTableSortingState, +} from "@medusajs/ui" +import { useMemo, useState } from "react" + +// ... + +const limit = 15 + +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]) + + // TODO add data fetching logic +} +``` + +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. + +You've also added two memoized variables: + +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. + +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: + +```tsx title="src/admin/routes/custom/page.tsx" +import { sdk } from "../../lib/config" +import { useQuery } from "@tanstack/react-query" +``` + +This imports the JS SDK instance and `useQuery` from [Tanstack Query](https://tanstack.com/query/latest). + +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]], +}) + +// TODO configure data table +``` + +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: + +- `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. + +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: + +```tsx title="src/admin/routes/custom/page.tsx" +import { + // ... + useDataTable, +} from "@medusajs/ui" +import { useNavigate } from "react-router-dom" +``` + +Then, replace the `TODO` in the component with the following: + +```tsx title="src/admin/routes/custom/page.tsx" +const navigate = useNavigate() + +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, + }, + onRowClick: (event, row) => { + // Handle row click, for example + navigate(`/products/${row.id}`) + }, +}) + +// TODO render component +``` + +The `useDataTable` hook accepts an object with the following properties: + +- columns: (\`array\`) The columns to display in the data table. You created this using the \`createDataTableColumnHelper\` utility. +- data: (\`array\`) The products fetched from the Medusa application. +- getRowId: (\`function\`) A function that returns the unique ID of a row. +- rowCount: (\`number\`) The total number of products that can be retrieved. This is used to determine the number of pages. +- isLoading: (\`boolean\`) A boolean that indicates if the data is being fetched. +- pagination: (\`object\`) An object to configure pagination. + + - state: (\`object\`) The pagination React state variable. + + - onPaginationChange: (\`function\`) A function that updates the pagination state. +- search: (\`object\`) An object to configure searching. + + - state: (\`string\`) The search query React state variable. + + - onSearchChange: (\`function\`) A function that updates the search query state. +- filtering: (\`object\`) An object to configure filtering. + + - state: (\`object\`) The filtering React state variable. + + - onFilteringChange: (\`function\`) A function that updates the filtering state. +- filters: (\`array\`) The filters to display in the data table. You created this using the \`createDataTableFilterHelper\` utility. +- sorting: (\`object\`) An object to configure sorting. + + - state: (\`object\`) The sorting React state variable. + + - onSortingChange: (\`function\`) A function that updates the sorting state. +- onRowClick: (\`function\`) A function that allows you to perform an action when the user clicks on a row. In this example, you navigate to the product's detail page. + + - event: (\`mouseevent\`) An instance of the \[MouseClickEvent]\(https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) object. + + - row: (\`object\`) The data of the row that was clicked. + +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" +import { + // ... + DataTable, +} from "@medusajs/ui" +import { SingleColumnLayout } from "../../layouts/single-column" +import { Container } from "../../components/container" +``` + +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: + +```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", + icon: ChatBubbleLeftRight, +}) + +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. + +### 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" + +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"} + + ) + }, + }), +] + +const filterHelper = createDataTableFilterHelper() + +const filters = [ + filterHelper.accessor("status", { + type: "select", + label: "Status", + options: [ + { + label: "Published", + value: "published", + }, + { + label: "Draft", + value: "draft", + }, + ], + }), +] + +const limit = 15 + +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, + }, + }) + + return ( + + + + + Products +
+ + + +
+
+ + +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + + +# 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. + +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 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 `Header` 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 ( + +
{ + alert("You clicked the button.") + }, + }, + }, + ]} + /> + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +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 + +Detail pages in the Medusa Admin show a JSON section to view the current page's details in JSON format. + +![Example of a JSON section in the admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295129/Medusa%20Resources/json_dtbsgm.png) + +To create a component that shows a JSON section in your customizations, create the file `src/admin/components/json-view-section.tsx` with the following content: + +```tsx title="src/admin/components/json-view-section.tsx" +import { + ArrowUpRightOnBox, + Check, + SquareTwoStack, + TriangleDownMini, + XMarkMini, +} from "@medusajs/icons" +import { + Badge, + Container, + Drawer, + Heading, + IconButton, + Kbd, +} from "@medusajs/ui" +import Primitive from "@uiw/react-json-view" +import { CSSProperties, MouseEvent, Suspense, useState } from "react" + +type JsonViewSectionProps = { + data: object + title?: string +} + +export const JsonViewSection = ({ data }: JsonViewSectionProps) => { + const numberOfKeys = Object.keys(data).length + + return ( + +
+ JSON + + {numberOfKeys} keys + +
+ + + + + + + +
+
+ + + + {numberOfKeys} + + + +
+
+ + esc + + + + + + +
+
+ +
+
} + > + + } /> + ( + null + )} + /> + ( + undefined + )} + /> + { + return ( + + {Object.keys(value as object).length} items + + ) + }} + /> + + + + + : + + { + return + }} + /> + + + +
+
+
+
+ ) +} + +type CopiedProps = { + style?: CSSProperties + value: object | undefined +} + +const Copied = ({ style, value }: CopiedProps) => { + const [copied, setCopied] = useState(false) + + const handler = (e: MouseEvent) => { + e.stopPropagation() + setCopied(true) + + if (typeof value === "string") { + navigator.clipboard.writeText(value) + } else { + const json = JSON.stringify(value, null, 2) + navigator.clipboard.writeText(json) + } + + setTimeout(() => { + setCopied(false) + }, 2000) + } + + const styl = { whiteSpace: "nowrap", width: "20px" } + + if (copied) { + 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" }`. + + +# Table - Admin Components + +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 as it provides features for sorting, filtering, pagination, and more with a simpler API. + +You can use the `Table` component from Medusa UI to display data in a table. It's mostly recommended for simpler tables. + +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/components/table.tsx" +import { useMemo } from "react" +import { Table as UiTable } from "@medusajs/ui" + +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 +} + +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) + } + } + + 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} + )} + + + ))} + + ) + })} + + + +
+ ) +} +``` + +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. + +It accepts the following props: + +- columns: (\`object\[]\`) The table's columns. + + - key: (\`string\`) The column's key in the passed \`data\` + + - label: (\`string\`) The column's label shown in the table. If not provided, the \`key\` is used. + + - 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. + +*** + +## Example + +Use the `Table` 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 { 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) + + return ( + +
{ + 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 +``` + +This widget also uses the [Container](../container.mdx) custom component. + +*** + +## Example With Data Fetching + +This section shows you how to use the `Table` component when fetching data from the Medusa application's API routes. + +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" + +const CustomPage = () => { + 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({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +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. + + +# 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 `SectionRow` component shows a title and a value in the same row. + +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 `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 { Container } from "../components/container" +import { Header } from "../components/header" +import { SectionRow } from "../components/section-row" + +const ProductWidget = () => { + return ( + +
+ + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +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. + + +# Single Column Layout - Admin Components + +The Medusa Admin has pages with a single column of content. + +This doesn't include the sidebar, only the main content. + +![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.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: + +```tsx title="src/admin/layouts/single-column.tsx" +export type SingleColumnLayoutProps = { + children: React.ReactNode +} + +export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { + return ( +
+ {children} +
+ ) +} +``` + +The `SingleColumnLayout` accepts the content in the `children` props. + +*** + +## Example + +Use the `SingleColumnLayout` 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 { SingleColumnLayout } from "../../layouts/single-column" +import { Header } from "../../components/header" + +const CustomPage = () => { + return ( + + +
+ + + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +export default CustomPage +``` + +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. + + +# Two Column Layout - Admin Components + +The Medusa Admin has pages with two columns of content. + +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) + +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: + +```tsx title="src/admin/layouts/two-column.tsx" +export type TwoColumnLayoutProps = { + firstCol: React.ReactNode + secondCol: React.ReactNode +} + +export const TwoColumnLayout = ({ + firstCol, + secondCol, +}: TwoColumnLayoutProps) => { + return ( +
+
+ {firstCol} +
+
+ {secondCol} +
+
+ ) +} +``` + +The `TwoColumnLayout` accepts two props: + +- `firstCol` indicating the content of the first column. +- `secondCol` indicating the content of the second column. + +*** + +## 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 ( + +
+ + } + secondCol={ + +
+ + } + /> + ) +} + +export const config = defineRouteConfig({ + label: "Custom", + icon: ChatBubbleLeftRight, +}) + +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. + + # Service Factory Reference This section of the documentation provides a reference of the methods generated for services extending the service factory (`MedusaService`), and how to use them. @@ -61845,6 +61845,692 @@ Some examples of method names: The reference uses only the operation name to refer to the method. +# 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). + + +# create Method - Service Factory Reference + +This method creates one or more records of the data model. + +## Create One Record + +```ts +const post = await postModuleService.createPosts({ + name: "My Post", + published_at: new Date(), + metadata: { + external_id: "1234", + }, +}) +``` + +If an object is passed of the method, an object of the created record is also returned. + +*** + +## Create Multiple Records + +```ts +const posts = await postModuleService.createPosts([ + { + name: "My Post", + published_at: new Date(), + }, + { + name: "My Other Post", + published_at: new Date(), + }, +]) +``` + +If an array is passed of the method, an array of the created records is also returned. + + +# list Method - Service Factory Reference + +This method retrieves a list of records. + +## Retrieve List of Records + +```ts +const posts = await postModuleService.listPosts() +``` + +If no parameters are passed, the method returns an array of the first `15` records. + +*** + +## Filter Records + +```ts +const posts = await postModuleService.listPosts({ + id: ["123", "321"], +}) +``` + +### Parameters + +To retrieve records matching a set of filters, pass an object of the filters as a first 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). + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Retrieve 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 posts = await postModuleService.listPosts({}, { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Select Properties + +```ts +const posts = await postModuleService.listPosts({}, { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. + +`select`'s value is an array of property names to retrieve. + +### Returns + +The method returns an array of the first `15` records matching the filters. + +*** + +## Paginate Relations + +```ts +const posts = await postModuleService.listPosts({}, { + take: 20, + skip: 10, +}) +``` + +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array of records. The number of records is less than or equal to `take`'s value. + +*** + +## Sort Records + +```ts +const posts = await postModuleService.listPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### 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. + +### Returns + +The method returns an array of the first `15` records matching the filters. + + +# listAndCount Method - Service Factory Reference + +This method retrieves a list of records with the total count. + +## Retrieve List of Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts() +``` + +If no parameters are passed, 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. + +*** + +## Filter Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({ + id: ["123", "321"], +}) +``` + +### Parameters + +To retrieve records matching a set of filters, pass an object of the filters as a first 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). + +### Returns + +The method returns an array with two items: + +1. The first is an array of the first `15` records retrieved matching the specified filters. +2. The second is the total count of records matching the specified filters. + +*** + +## Retrieve 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 [posts, count] = await postModuleService.listAndCountPosts({}, { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve records with their relations, pass as a second parameter an object having a `relations` property. Its value is an array of relation 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. + +*** + +## Select Properties + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. + +`select`'s value is an array of property names to retrieve. + +### 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. + +*** + +## Paginate Relations + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + take: 20, + skip: 10, +}) +``` + +### Parameters + +To paginate the returned records, the second object parameter accepts the following properties: + +- `take`: a number indicating how many records to retrieve. By default, it's `15`. +- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. + +### Returns + +The method returns an array with two items: + +1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value. +2. The second is the total count of records. + +*** + +## Sort Records + +```ts +const [posts, count] = await postModuleService.listAndCountPosts({}, { + order: { + name: "ASC", + }, +}) +``` + +### 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. + +### 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. + + +# restore Method - Service Factory Reference + +This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md). + +## Restore One Record + +```ts +const restoredPosts = await postModuleService.restorePosts("123") +``` + +### Parameters + +To restore one record, pass its ID as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: ["123"], +} +``` + +*** + +## Restore Multiple Records + +```ts +const restoredPosts = await postModuleService.restorePosts([ + "123", + "321", +]) +``` + +### Parameters + +To restore multiple records, pass an array of IDs as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: [ + "123", + "321", + ], +} +``` + +*** + +## Restore Records Matching Filters + +```ts +const restoredPosts = await postModuleService.restorePosts({ + name: "My Post", +}) +``` + +### Parameters + +To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. + +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 object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. + +For example, the returned object of the above example is: + +```ts +restoredPosts = { + post_id: [ + "123", + ], +} +``` + + +# 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 the record as an object. + + +# softDelete Method - Service Factory Reference + +This method soft deletes one or more records of the data model. + +## Soft Delete One Record + +```ts +const deletedPosts = await postModuleService.softDeletePosts( + "123" +) +``` + +### Parameters + +To soft delete a record, pass its ID as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: ["123"], +} +``` + +*** + +## Soft Delete Multiple Records + +```ts +const deletedPosts = await postModuleService.softDeletePosts([ + "123", + "321", +]) +``` + +### Parameters + +To soft delete multiple records, pass an array of IDs as a parameter of the method. + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: [ + "123", + "321", + ], +} +``` + +*** + +## Soft Delete Records Matching Filters + +```ts +const deletedPosts = await postModuleService.softDeletePosts({ + name: "My Post", +}) +``` + +### Parameters + +To soft 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). + +### Returns + +The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. + +For example, the returned object of the above example is: + +```ts +deletedPosts = { + post_id: ["123"], +} +``` + + +# 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. @@ -62132,692 +62818,6 @@ The following operators are supported by the service factory filtering mechanism |\`$not\`|Inverts the logic of a condition. For example, | -# create Method - Service Factory Reference - -This method creates one or more records of the data model. - -## Create One Record - -```ts -const post = await postModuleService.createPosts({ - name: "My Post", - published_at: new Date(), - metadata: { - external_id: "1234", - }, -}) -``` - -If an object is passed of the method, an object of the created record is also returned. - -*** - -## Create Multiple Records - -```ts -const posts = await postModuleService.createPosts([ - { - name: "My Post", - published_at: new Date(), - }, - { - name: "My Other Post", - published_at: new Date(), - }, -]) -``` - -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). - - -# listAndCount Method - Service Factory Reference - -This method retrieves a list of records with the total count. - -## Retrieve List of Records - -```ts -const [posts, count] = await postModuleService.listAndCountPosts() -``` - -If no parameters are passed, 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. - -*** - -## Filter Records - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({ - id: ["123", "321"], -}) -``` - -### Parameters - -To retrieve records matching a set of filters, pass an object of the filters as a first 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). - -### Returns - -The method returns an array with two items: - -1. The first is an array of the first `15` records retrieved matching the specified filters. -2. The second is the total count of records matching the specified filters. - -*** - -## Retrieve 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 [posts, count] = await postModuleService.listAndCountPosts({}, { - relations: ["author"], -}) -``` - -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. Its value is an array of relation 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. - -*** - -## Select Properties - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - select: ["id", "name"], -}) -``` - -### Parameters - -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. - -### 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. - -*** - -## Paginate Relations - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - take: 20, - skip: 10, -}) -``` - -### Parameters - -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. - -### Returns - -The method returns an array with two items: - -1. The first is an array of the records retrieved. The number of records is less than or equal to `take`'s value. -2. The second is the total count of records. - -*** - -## Sort Records - -```ts -const [posts, count] = await postModuleService.listAndCountPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### 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. - -### 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. - - -# restore Method - Service Factory Reference - -This method restores one or more records of the data model that were [soft-deleted](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/service-factory-reference/methods/soft-delete/index.html.md). - -## Restore One Record - -```ts -const restoredPosts = await postModuleService.restorePosts("123") -``` - -### Parameters - -To restore one record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: ["123"], -} -``` - -*** - -## Restore Multiple Records - -```ts -const restoredPosts = await postModuleService.restorePosts([ - "123", - "321", -]) -``` - -### Parameters - -To restore multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - "321", - ], -} -``` - -*** - -## Restore Records Matching Filters - -```ts -const restoredPosts = await postModuleService.restorePosts({ - name: "My Post", -}) -``` - -### Parameters - -To restore records matching a set of filters, pass an object of fitlers as a parameter of the method. - -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 object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of restored records' IDs. - -For example, the returned object of the above example is: - -```ts -restoredPosts = { - post_id: [ - "123", - ], -} -``` - - -# list Method - Service Factory Reference - -This method retrieves a list of records. - -## Retrieve List of Records - -```ts -const posts = await postModuleService.listPosts() -``` - -If no parameters are passed, the method returns an array of the first `15` records. - -*** - -## Filter Records - -```ts -const posts = await postModuleService.listPosts({ - id: ["123", "321"], -}) -``` - -### Parameters - -To retrieve records matching a set of filters, pass an object of the filters as a first 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). - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Retrieve 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 posts = await postModuleService.listPosts({}, { - relations: ["author"], -}) -``` - -### Parameters - -To retrieve records with their relations, pass as a second parameter an object having a `relations` property. `relations`'s value is an array of relation names. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Select Properties - -```ts -const posts = await postModuleService.listPosts({}, { - select: ["id", "name"], -}) -``` - -### Parameters - -By default, retrieved records have all their properties. To select specific properties to retrieve, pass in the second object parameter a `select` property. - -`select`'s value is an array of property names to retrieve. - -### Returns - -The method returns an array of the first `15` records matching the filters. - -*** - -## Paginate Relations - -```ts -const posts = await postModuleService.listPosts({}, { - take: 20, - skip: 10, -}) -``` - -### Parameters - -To paginate the returned records, the second object parameter accepts the following properties: - -- `take`: a number indicating how many records to retrieve. By default, it's `15`. -- `skip`: a number indicating how many records to skip before the retrieved records. By default, it's `0`. - -### Returns - -The method returns an array of records. The number of records is less than or equal to `take`'s value. - -*** - -## Sort Records - -```ts -const posts = await postModuleService.listPosts({}, { - order: { - name: "ASC", - }, -}) -``` - -### 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. - -### Returns - -The method returns an array of the first `15` records matching the filters. - - -# 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 the record as an object. - - -# 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. - - -# softDelete Method - Service Factory Reference - -This method soft deletes one or more records of the data model. - -## Soft Delete One Record - -```ts -const deletedPosts = await postModuleService.softDeletePosts( - "123" -) -``` - -### Parameters - -To soft delete a record, pass its ID as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: ["123"], -} -``` - -*** - -## Soft Delete Multiple Records - -```ts -const deletedPosts = await postModuleService.softDeletePosts([ - "123", - "321", -]) -``` - -### Parameters - -To soft delete multiple records, pass an array of IDs as a parameter of the method. - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: [ - "123", - "321", - ], -} -``` - -*** - -## Soft Delete Records Matching Filters - -```ts -const deletedPosts = await postModuleService.softDeletePosts({ - name: "My Post", -}) -``` - -### Parameters - -To soft 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). - -### Returns - -The method returns an object, whose keys are of the format `{camel_case_data_model_name}_id`, and their values are arrays of soft-deleted records' IDs. - -For example, the returned object of the above example is: - -```ts -deletedPosts = { - post_id: ["123"], -} -``` - -

Just Getting Started?

@@ -63278,125 +63278,6 @@ How to install and setup Medusa UI. -# Medusa Admin Extension - -How to install and use Medusa UI for building Admin extensions. - -## Installation - -*** - -The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. - -If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. - -```bash -npm install @medusajs/ui -``` - -## Configuration - -*** - -The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. - - -# Standalone Project - -How to install and use Medusa UI in a standalone project. - -## Installation - -*** - -Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. - -### Install Medusa UI - -Install the React UI library with the following command: - -```bash -npm install @medusajs/ui -``` - -### Configuring Tailwind CSS - -The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. -For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). - -All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. -You can install it with the following command: - -```bash -npm install @medusajs/ui-preset -``` - -After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: - -```tsx -module.exports = { - presets: [require("@medusajs/ui-preset")], - // ... -} -``` - -In order for the styles to be applied correctly to the components, you will also need to ensure that -`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: - -```tsx -module.exports = { - content: [ - // ... - "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", - ], - // ... -} -``` - -If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: - -```tsx -const path = require("path") - -const uiPath = path.resolve( - require.resolve("@medusajs/ui"), - "../..", - "\*_/_.{js,jsx,ts,tsx}" -) - -module.exports = { - content: [ - // ... - uiPath, - ], - // ... -} - -``` - -## Start building - -*** - -You are now ready to start building your application with Medusa UI. You can import the components like so: - -```tsx -import { Button, Drawer } from "@medusajs/ui" -``` - -## Updating UI Packages - -*** - -Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. - -To update these packages, update their version in your `package.json` file and re-install dependencies. For example: - -```bash -npm install @medusajs/ui -``` - - # Alert A component for displaying important messages. @@ -69818,6 +69699,125 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - disableHoverableContent: (boolean) When \`true\`, trying to hover the content will result in the tooltip closing as the pointer leaves the trigger. +# Medusa Admin Extension + +How to install and use Medusa UI for building Admin extensions. + +## Installation + +*** + +The `@medusajs/ui` package is a already installed as a dependency of the `@medusajs/admin` package. Due to this you can simply import the package and use it in your local Admin extensions. + +If you are building a Admin extension as part of a Medusa plugin, you can install the package as a dependency of your plugin. + +```bash +npm install @medusajs/ui +``` + +## Configuration + +*** + +The configuration of the UI package is handled by the `@medusajs/admin` package. Therefore, you do not need to any additional configuration to use the UI package in your Admin extensions. + + +# Standalone Project + +How to install and use Medusa UI in a standalone project. + +## Installation + +*** + +Medusa UI is a React UI library and while it's intended for usage within Medusa projects, it can also be used in any React project. + +### Install Medusa UI + +Install the React UI library with the following command: + +```bash +npm install @medusajs/ui +``` + +### Configuring Tailwind CSS + +The components are styled using Tailwind CSS, and in order to use them, you will need to install Tailwind CSS in your project as well. +For more information on how to install Tailwind CSS, please refer to the [Tailwind CSS documentation](https://tailwindcss.com/docs/installation). + +All of the classes used for Medusa UI are shipped as a Tailwind CSS customization. +You can install it with the following command: + +```bash +npm install @medusajs/ui-preset +``` + +After you have installed Tailwind CSS and the Medusa UI preset, you need to add the following to your `tailwind.config.js`file: + +```tsx +module.exports = { + presets: [require("@medusajs/ui-preset")], + // ... +} +``` + +In order for the styles to be applied correctly to the components, you will also need to ensure that +`@medusajs/ui` is included in the content field of your `tailwind.config.js` file: + +```tsx +module.exports = { + content: [ + // ... + "./node_modules/@medusajs/ui/dist/**/*.{js,jsx,ts,tsx}", + ], + // ... +} +``` + +If you are working within a monorepo, you may need to add the path to the `@medusajs/ui` package in your `tailwind.config.js` like so: + +```tsx +const path = require("path") + +const uiPath = path.resolve( + require.resolve("@medusajs/ui"), + "../..", + "\*_/_.{js,jsx,ts,tsx}" +) + +module.exports = { + content: [ + // ... + uiPath, + ], + // ... +} + +``` + +## Start building + +*** + +You are now ready to start building your application with Medusa UI. You can import the components like so: + +```tsx +import { Button, Drawer } from "@medusajs/ui" +``` + +## Updating UI Packages + +*** + +Medusa's design-system packages, including `@medusajs/ui`, `@medusajs/ui-preset`, and `@medusajs/ui-icons`, are versioned independently. However, they're still part of the latest Medusa release. So, you can browse the [release notes](https://github.com/medusajs/medusa/releases) to see if there are any breaking changes to these packages. + +To update these packages, update their version in your `package.json` file and re-install dependencies. For example: + +```bash +npm install @medusajs/ui +``` + + # clx Utility function for working with classNames. diff --git a/www/apps/resources/app/infrastructure-modules/file/local/page.mdx b/www/apps/resources/app/infrastructure-modules/file/local/page.mdx index 3b98c084de..c212c0dde5 100644 --- a/www/apps/resources/app/infrastructure-modules/file/local/page.mdx +++ b/www/apps/resources/app/infrastructure-modules/file/local/page.mdx @@ -10,7 +10,8 @@ The Local File Module Provider stores files uploaded to your Medusa application -The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](../s3/page.mdx) in production instead. +- The Local File Module Provider is only for development purposes. Use the [S3 File Module Provider](../s3/page.mdx) in production instead. +- The Local File Module Provider will only read files uploaded through Medusa. It will not read files uploaded manually to the `static` (or other configured) directory. diff --git a/www/apps/resources/app/infrastructure-modules/notification/send-notification/page.mdx b/www/apps/resources/app/infrastructure-modules/notification/send-notification/page.mdx index ad81c85545..fbaf9742fc 100644 --- a/www/apps/resources/app/infrastructure-modules/notification/send-notification/page.mdx +++ b/www/apps/resources/app/infrastructure-modules/notification/send-notification/page.mdx @@ -14,11 +14,11 @@ export const metadata = { # {metadata.title} -In this guide, you'll learn how to send a notification using the Notification Module. +In this guide, you'll learn about the different ways to send notifications using the Notification Module. -## Use the Create Method +## Using the Create Method -In your resource, such as a subscriber, resolve the Notification Module's main service and use its `create` method: +In your resource, such as a [subscriber](!docs!/learn/fundamentals/events-and-subscribers), resolve the Notification Module's main service and use its `create` method: export const highlights = [ ["12", "notificationModuleService", "Resolve the Notification Module."], @@ -56,7 +56,7 @@ export default async function productCreateHandler({ container.resolve(Modules.NOTIFICATION) await notificationModuleService.createNotifications({ - to: "shahednasser@gmail.com", + to: "user@gmail.com", channel: "email", template: "product-created", data, @@ -103,3 +103,78 @@ The `create` method accepts an object or an array of objects having the followin /> For a full list of properties accepted, refer to [this guide](/references/notification-provider-module#create). + +--- + +## Using the sendNotifcationStep + +If you want to send a notification as part of a workflow, You can use the [sendNotifcationStep](!resources!/references/medusa-workflows/steps/sendNotificationsStep) in your workflow. + +For example: + +```ts title="src/workflows/send-email.ts" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { + sendNotificationsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + id: string +} + +export const sendEmailWorkflow = createWorkflow( + "send-email-workflow", + ({ id }: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "*", + "variants.*", + ], + filters: { + id, + }, + }) + + sendNotificationsStep({ + to: "user@gmail.com", + channel: "email", + template: "product-created", + data: { + product_title: product[0].title, + product_image: product[0].images[0]?.url, + }, + }) + } +) +``` + +For a full list of input properties accepted, refer to the [sendNotifcationStep](!resources!/references/medusa-workflows/steps/sendNotificationsStep) reference. + +You can then execute this workflow in a subscriber, API route, or scheduled job. + +For example, you can execute it when a product is created: + +```ts title="src/subscribers/product-created.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { sendEmailWorkflow } from "../workflows/send-email" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendEmailWorkflow(container).run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` diff --git a/www/apps/resources/app/infrastructure-modules/notification/sendgrid/page.mdx b/www/apps/resources/app/infrastructure-modules/notification/sendgrid/page.mdx index 7ce775419c..f8be043d17 100644 --- a/www/apps/resources/app/infrastructure-modules/notification/sendgrid/page.mdx +++ b/www/apps/resources/app/infrastructure-modules/notification/sendgrid/page.mdx @@ -8,8 +8,6 @@ export const metadata = { The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers. ---- - ## Register the SendGrid Notification Module ) { const notificationModuleService = container.resolve(Modules.NOTIFICATION) - const productModuleService = container.resolve(Modules.PRODUCT) + const query = container.resolve("query") - const product = await productModuleService.retrieveProduct(data.id) + const { data: [product] } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: data.id, + }, + }) await notificationModuleService.createNotifications({ to: "test@gmail.com", @@ -161,8 +168,8 @@ export const config: SubscriberConfig = { In this subscriber, you: -- Resolve the Notification and Product Modules' main services from the [Medusa container](!docs!/learn/fundamentals/medusa-container). -- Retrieve the product's details to pass them to the template in SendGrid. +- Resolve the Notification Module's main service and [Query](!docs!/learn/fundamentals/module-links/query) from the [Medusa container](!docs!/learn/fundamentals/medusa-container). +- Retrieve the product's details using Query to pass them to the template in SendGrid. - Use the `createNotifications` method of the Notification Module's main service to create a notification to be sent to the specified email. By specifying the `email` channel, the SendGrid Notification Module Provider is used to send the notification. - The `template` property of the `createNotifications` method's parameter specifies the ID of the template defined in SendGrid. - The `data` property allows you to pass data to the template in SendGrid. For example, the product's title and image. @@ -174,3 +181,85 @@ npm run dev ``` And create a product either using the [API route](!api!/admin#products_postproducts) or the [Medusa Admin](!user-guide!/products/create). This runs the subscriber and sends an email using SendGrid. + +### Other Events to Handle + +Medusa emits other events that you can handle to send notifications using the SendGrid Notification Module Provider, such as `order.placed` when an order is placed. + +Refer to the [Events Reference](/references/events) for a complete list of events emitted by Medusa. + +### Sending Emails with SendGrid in Workflows + +You can also send an email using SendGrid in any [workflow](!docs!/learn/fundamentals/workflows). This allows you to send emails within your custom flows. + +You can use the [sendNotifcationStep](!resources!/references/medusa-workflows/steps/sendNotificationsStep) in your workflow to send an email using SendGrid. + +For example: + +```ts title="src/workflows/send-email.ts" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { + sendNotificationsStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + id: string +} + +export const sendEmailWorkflow = createWorkflow( + "send-email-workflow", + ({ id }: WorkflowInput) => { + const { data: products } = useQueryGraphStep({ + entity: "product", + fields: [ + "*", + "variants.*", + ], + filters: { + id, + }, + }) + + sendNotificationsStep({ + to: "test@gmail.com", + channel: "email", + template: "product-created", + data: { + product_title: product[0].title, + product_image: product[0].images[0]?.url, + }, + }) + } +) +``` + +This workflow works similarly to the subscriber. It retrieves the product's details using Query and sends an email using SendGrid (by specifying the `email` channel) to the `test@gmail.com` email. + +You can also execute this workflow in a subscriber. For example, you can execute it when a product is created: + +```ts title="src/subscribers/product-created.ts" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { sendEmailWorkflow } from "../workflows/send-email" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendEmailWorkflow(container).run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid. diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 7071312b1f..e2e2d5c232 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -196,7 +196,7 @@ export const generatedEditDates = { "app/commerce-modules/auth/create-actor-type/page.mdx": "2025-05-09T07:09:41.615Z", "app/infrastructure-modules/page.mdx": "2025-04-17T08:29:00.388Z", "app/infrastructure-modules/workflow-engine/redis/page.mdx": "2024-11-19T16:37:47.262Z", - "app/infrastructure-modules/notification/sendgrid/page.mdx": "2025-03-27T17:39:10.840Z", + "app/infrastructure-modules/notification/sendgrid/page.mdx": "2025-05-15T15:38:26.008Z", "app/commerce-modules/api-key/concepts/page.mdx": "2024-10-07T13:59:37.529Z", "app/infrastructure-modules/workflow-engine/page.mdx": "2025-04-17T08:29:00.881Z", "app/infrastructure-modules/cache/page.mdx": "2025-04-17T08:29:00.741Z", @@ -210,7 +210,7 @@ export const generatedEditDates = { "app/infrastructure-modules/cache/in-memory/page.mdx": "2024-11-19T16:37:47.261Z", "app/infrastructure-modules/notification/local/page.mdx": "2024-11-19T16:37:47.262Z", "app/infrastructure-modules/file/local/page.mdx": "2024-11-19T16:37:47.261Z", - "app/infrastructure-modules/notification/send-notification/page.mdx": "2025-03-27T14:53:13.311Z", + "app/infrastructure-modules/notification/send-notification/page.mdx": "2025-05-15T15:38:18.956Z", "app/infrastructure-modules/file/page.mdx": "2025-04-17T08:29:00.672Z", "app/infrastructure-modules/event/page.mdx": "2025-04-17T08:29:00.488Z", "app/infrastructure-modules/cache/create/page.mdx": "2025-03-27T14:53:13.309Z",