From 41c29ffaddbb19dc83fd58dbccc2d82352c033d6 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Tue, 27 May 2025 12:01:04 +0300 Subject: [PATCH] docs: phone authentication + otp guide (#12544) * initial * docs: phone authentication guide * small fix * Twilio SMS -> Twilio * vale fix * fix * fix to auth sidebar * fixes * generate --- www/apps/book/public/llms-full.txt | 37079 ++++++++-------- .../tutorials/phone-auth/page.mdx | 2300 + www/apps/resources/app/integrations/page.mdx | 8 + www/apps/resources/generated/files-map.mjs | 4 + .../generated-commerce-modules-sidebar.mjs | 18 +- .../generated-how-to-tutorials-sidebar.mjs | 9 + .../generated-integrations-sidebar.mjs | 8 + www/apps/resources/sidebars/auth.mjs | 2 +- .../resources/sidebars/how-to-tutorials.mjs | 7 + www/apps/resources/sidebars/integrations.mjs | 5 + www/packages/tags/src/tags/auth.ts | 4 + www/packages/tags/src/tags/customer.ts | 4 + www/packages/tags/src/tags/server.ts | 4 + www/packages/tags/src/tags/tutorial.ts | 4 + 14 files changed, 21950 insertions(+), 17506 deletions(-) create mode 100644 www/apps/resources/app/how-to-tutorials/tutorials/phone-auth/page.mdx diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 26f3b0fa7f..cd2acfbec3 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -1420,6 +1420,388 @@ npx medusa db:migrate ``` +# Medusa's Architecture + +In this chapter, you'll learn about the architectural layers in Medusa. + +Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). + +## HTTP, Workflow, and Module Layers + +Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. + +In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: + +1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. +2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. +3. Modules: Workflows use domain-specific modules for resource management. +4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. + +These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) + +*** + +## Database Layer + +The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) + +*** + +## Third-Party Integrations Layer + +Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). + +Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + +### Commerce Modules + +[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. + +You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. + +You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. + +![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) + +### Infrastructure Modules + +[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: + +- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider. +- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). +- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. +- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. +- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. +- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. +- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. + +All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. + +![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) + +*** + +## Full Diagram of Medusa's Architecture + +The following diagram illustrates Medusa's architecture including all its layers. + +![Full diagram illustrating Medusa's architecture combining all the different layers.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727174897/Medusa%20Book/architectural-diagram-full.jpg) + + +# 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 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. + + # Configure Instrumentation In this chapter, you'll learn about observability in Medusa and how to configure instrumentation with OpenTelemetry. @@ -1766,443 +2148,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. -# 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 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. - - -# 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). - -After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: - -- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. -- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). - -From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard - -*** - -## Next Chapters: View Brands in Dashboard - -In the next chapters, you'll continue with the brands example to: - -- Add a new section to the product details page that shows the product's brand. -- Add a new page in the dashboard that shows all brands in the store. - - -# Re-Use Customizations with Plugins - -In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. - -You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. - -To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. - -![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) - -Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. - -To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - - -# Customizations Next Steps: Learn the Fundamentals - -The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. - -The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. - -## Useful Guides - -The following guides and references are useful for your development journey: - -3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them. -4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. -5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. -6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. - -*** - -## More Examples in Recipes - -In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. - - -# Extend Core Commerce Features - -In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. - -In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run. - -The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: - -- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. -- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. -- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. - -*** - -## Next Chapters: Link Brands to Products Example - -The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to: - -- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). -- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product. -- Retrieve a product's associated brand's details. - - -# Integrate Third-Party Systems - -Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. - -The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. - -In Medusa, you integrate a third-party system by: - -1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. -2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. -3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. - -*** - -## Next Chapters: Sync Brands Example - -In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: - -1. Integrate a dummy third-party CMS in the Brand Module. -2. Sync brands to the CMS when a brand is created. -3. Sync brands from the CMS at a daily schedule. - - # Admin Development In this chapter, you'll learn about the Medusa Admin dashboard and the possible ways to customize it. @@ -2249,65 +2194,6 @@ Refer to the [Medusa UI documentation](https://docs.medusajs.com/ui/index.html.m To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. -# API Routes - -In this chapter, you’ll learn what API Routes are and how to create them. - -## What is an API Route? - -An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. - -The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. - -*** - -## How to Create an API Route? - -An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. - -![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) - -Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). - -For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "[GET] Hello world!", - }) -} -``` - -### Test API Route - -To test the API route above, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, send a `GET` request to the `/hello-world` API Route: - -```bash -curl http://localhost:9000/hello-world -``` - -*** - -## When to Use API Routes - -You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. - - # Custom CLI Scripts In this chapter, you'll learn how to create and execute custom scripts from Medusa's CLI tool. @@ -2378,6 +2264,169 @@ npx medusa exec ./src/scripts/my-script.ts arg1 arg2 ``` +# Data Models + +In this chapter, you'll learn what a data model is and how to create a data model. + +## What is a Data Model? + +A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. + +You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. + +*** + +## How to Create a Data Model + +In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. + +So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: + +![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) + +```ts title="src/modules/blog/models/post.ts" +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + title: model.text(), +}) + +export default Post +``` + +You define the data model using the `define` method of the DML. It accepts two parameters: + +1. The first one is the name of the data model's table in the database. Use snake-case names. +2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. + - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. + +The code snippet above defines a `Post` data model with `id` and `title` properties. + +*** + +## Generate Migrations + +After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: + +If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. + +```bash +npx medusa db:generate blog +``` + +The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: + +```ts +import { Migration } from "@mikro-orm/migrations" + +export class Migration20241121103722 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"post\" cascade;") + } + +} +``` + +In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. + +### Run Migrations + +To reflect the changes in the generated migration file on the database, run the `db:migrate` command: + +If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. + +```bash +npx medusa db:migrate +``` + +This creates the `post` table in the database. + +### Migrations on Data Model Changes + +Whenever you make a change to a data model, you must generate and run the migrations. + +For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. + +*** + +## Manage Data Models + +Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. + +For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. + +Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. + + +# API Routes + +In this chapter, you’ll learn what API Routes are and how to create them. + +## What is an API Route? + +An API Route is an endpoint. It exposes commerce features to external applications, such as storefronts, the admin dashboard, or third-party systems. + +The Medusa core application provides a set of admin and store API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. + +*** + +## How to Create an API Route? + +An API Route is created in a TypeScript or JavaScript file under the `src/api` directory of your Medusa application. The file’s name must be `route.ts` or `route.js`. + +![Example of API route in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732808645/Medusa%20Book/route-dir-overview_dqgzmk.jpg) + +Each file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). + +For example, to create a `GET` API Route at `/hello-world`, create the file `src/api/hello-world/route.ts` with the following content: + +```ts title="src/api/hello-world/route.ts" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "[GET] Hello world!", + }) +} +``` + +### Test API Route + +To test the API route above, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, send a `GET` request to the `/hello-world` API Route: + +```bash +curl http://localhost:9000/hello-world +``` + +*** + +## When to Use API Routes + +You're exposing custom functionality to be used by a storefront, admin dashboard, or any external application. + + # Environment Variables In this chapter, you'll learn how environment variables are loaded in Medusa. @@ -2553,108 +2602,154 @@ Medusa provides two Event Modules out of the box: Medusa's [architecture](https://docs.medusajs.com/learn/introduction/architecture/index.html.md) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](https://docs.medusajs.com/resources/infrastructure-modules/event/create/index.html.md). -# Data Models +# Medusa Container -In this chapter, you'll learn what a data model is and how to create a data model. +In this chapter, you’ll learn about the Medusa container and how to use it. -## What is a Data Model? +## What is the Medusa Container? -A data model represents a table in the database. You create data models using Medusa's data modeling language (DML). It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations. +The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. -You create a data model in a [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). The module's service provides the methods to store and manage those data models. Then, you can resolve the module's service in other customizations, such as a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), to manage the data models' records. +In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. -*** +Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. -## How to Create a Data Model +For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: -In a module, you can create a data model in a TypeScript or JavaScript file under the module's `models` directory. +```ts highlights={highlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" -So, for example, assuming you have a Blog Module at `src/modules/blog`, you can create a `Post` data model by creating the `src/modules/blog/models/post.ts` file with the following content: +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const query = req.scope.resolve("query") -![Updated directory overview after adding the data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732806790/Medusa%20Book/blog-dir-overview-1_jfvovj.jpg) - -```ts title="src/modules/blog/models/post.ts" -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - title: model.text(), -}) - -export default Post -``` - -You define the data model using the `define` method of the DML. It accepts two parameters: - -1. The first one is the name of the data model's table in the database. Use snake-case names. -2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods, such as `text` and `id`. - - Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`, so you don't need to add them manually. - -The code snippet above defines a `Post` data model with `id` and `title` properties. - -*** - -## Generate Migrations - -After you create a data model in a module, then [register that module in your Medusa configurations](https://docs.medusajs.com/learn/fundamentals/modules#4-add-module-to-medusas-configurations/index.html.md), you must generate a migration to create the data model's table in the database. - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -For example, to generate a migration for the Blog Module, run the following command in your Medusa application's directory: - -If you're creating the module in a plugin, use the [plugin:db:generate command](https://docs.medusajs.com/resources/medusa-cli/commands/plugin#plugindbgenerate/index.html.md) instead. - -```bash -npx medusa db:generate blog -``` - -The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory `src/modules/blog/migrations` similar to the following: - -```ts -import { Migration } from "@mikro-orm/migrations" - -export class Migration20241121103722 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"post\" cascade;") - } + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + res.json({ + products, + }) } ``` -In the migration class, the `up` method creates the table `post` and defines its columns using PostgreSQL syntax. The `down` method drops the table. +The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. -### Run Migrations - -To reflect the changes in the generated migration file on the database, run the `db:migrate` command: - -If you're creating the module in a plugin, run this command on the Medusa application that the plugin is installed in. - -```bash -npx medusa db:migrate -``` - -This creates the `post` table in the database. - -### Migrations on Data Model Changes - -Whenever you make a change to a data model, you must generate and run the migrations. - -For example, if you add a new column to the `Post` data model, you must generate a new migration and run it. +You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). *** -## Manage Data Models +## List of Resources Registered in the Medusa Container -Your module's service should extend the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md), which generates data-management methods for your module's data models. +Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) -For example, the Blog Module's service would have methods like `retrievePost` and `createPosts`. +*** -Refer to the [Service Factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) chapter to learn more about how to extend the service factory and manage data models, and refer to the [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md) for the full list of generated methods and how to use them. +## How to Resolve From the Medusa Container + +This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. + +### Subscriber + +A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={subscriberHighlights} +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: data.id, + }, + }) + + console.log(`You created a product with the title ${products[0].title}`) +} + +export const config: SubscriberConfig = { + event: `product.created`, +} +``` + +### Scheduled Job + +A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={scheduledJobHighlights} +import { MedusaContainer } from "@medusajs/framework/types" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export default async function myCustomJob( + container: MedusaContainer +) { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + + console.log( + `You have ${products.length} matching your filters.` + ) +} + +export const config = { + name: "every-minute-message", + // execute every minute + schedule: "* * * * *", +} +``` + +### Workflow Step + +A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: + +```ts highlights={workflowStepsHighlight} +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +const step1 = createStep("step-1", async (_, { container }) => { + const query = container.resolve(ContainerRegistrationKeys.QUERY) + + const { data: products } = await query.graph({ + entity: "product", + fields: ["*"], + filters: { + id: "prod_123", + }, + }) + + return new StepResponse(products) +}) +``` + +### Module Services and Loaders + +A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. + +Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). # Framework Overview @@ -3427,156 +3522,6 @@ To learn more about the different concepts useful for building plugins, check ou - [Plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md) -# Medusa Container - -In this chapter, you’ll learn about the Medusa container and how to use it. - -## What is the Medusa Container? - -The Medusa container is a registry of Framework and commerce tools that's accessible across your application. Medusa automatically registers these tools in the container, including custom ones that you've built, so that you can use them in your customizations. - -In other platforms, if you have a resource A (for example, a class) that depends on a resource B, you have to manually add resource B to the container or specify it beforehand as A's dependency, which is often done in a file separate from A's code. This becomes difficult to manage as you maintain larger applications with many changing dependencies. - -Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. - -For example, consider you're creating an API route that retrieves products based on filters using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: - -```ts highlights={highlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const query = req.scope.resolve("query") - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - res.json({ - products, - }) -} -``` - -The API route accepts as a first parameter a request object that has a `scope` property, which is the Medusa container. It has a `resolve` method that resolves a resource from the container by the key it's registered with. - -You can learn more about how Query works in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -*** - -## List of Resources Registered in the Medusa Container - -Find a full list of the registered resources and their registration key in [this reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) - -*** - -## How to Resolve From the Medusa Container - -This section gives quick examples of how to resolve resources from the Medusa container in customizations other than an API route, which is covered in the section above. - -### Subscriber - -A [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={subscriberHighlights} -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: data.id, - }, - }) - - console.log(`You created a product with the title ${products[0].title}`) -} - -export const config: SubscriberConfig = { - event: `product.created`, -} -``` - -### Scheduled Job - -A [scheduled job](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={scheduledJobHighlights} -import { MedusaContainer } from "@medusajs/framework/types" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -export default async function myCustomJob( - container: MedusaContainer -) { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - console.log( - `You have ${products.length} matching your filters.` - ) -} - -export const config = { - name: "every-minute-message", - // execute every minute - schedule: "* * * * *", -} -``` - -### Workflow Step - -A [step in a workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: - -```ts highlights={workflowStepsHighlight} -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ContainerRegistrationKeys } from "@medusajs/framework/utils" - -const step1 = createStep("step-1", async (_, { container }) => { - const query = container.resolve(ContainerRegistrationKeys.QUERY) - - const { data: products } = await query.graph({ - entity: "product", - fields: ["*"], - filters: { - id: "prod_123", - }, - }) - - return new StepResponse(products) -}) -``` - -### Module Services and Loaders - -A [module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. - -Learn more about the module's container in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md). - - # Define Module Link In this chapter, you’ll learn what a module link is and how to define one. @@ -3830,100 +3775,6 @@ For example, in a plugin, you can define a module that integrates a third-party The next chapter explains how you can create and publish a plugin. -# 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. @@ -4224,170 +4075,121 @@ 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. -# Medusa's Architecture +# Scheduled Jobs -In this chapter, you'll learn about the architectural layers in Medusa. +In this chapter, you’ll learn about scheduled jobs and how to use them. -Find the full architectural diagram at the [end of this chapter](#full-diagram-of-medusas-architecture). +## What is a Scheduled Job? -## HTTP, Workflow, and Module Layers +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. -Medusa is a headless commerce platform. So, storefronts, admin dashboards, and other clients consume Medusa's functionalities through its API routes. +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. -In a common Medusa application, requests go through four layers in the stack. In order of entry, those are: +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. -1. API Routes (HTTP): Our API Routes are the typical entry point. The Medusa server is based on Express.js, which handles incoming requests. It can also connect to a Redis database that stores the server session data. -2. Workflows: API Routes consume workflows that hold the opinionated business logic of your application. -3. Modules: Workflows use domain-specific modules for resource management. -4. Data store: Modules query the underlying datastore, which is a PostgreSQL database in common cases. - -These layers of stack can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). - -![This diagram illustrates the entry point of requests into the Medusa application through API routes. It shows a storefront and an admin that can send a request to the HTTP layer. The HTTP layer then uses workflows to handle the business logic. Finally, the workflows use modules to query and manipulate data in the data stores.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175296/Medusa%20Book/http-layer_sroafr.jpg) +- 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. *** -## Database Layer +## How to Create a Scheduled Job? -The Medusa application injects into each module, including your [custom modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), a connection to the configured PostgreSQL database. Modules use that connection to read and write data to the database. +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. -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +For example, create the file `src/jobs/hello-world.ts` with the following content: -![This diagram illustrates how modules connect to the database.](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175379/Medusa%20Book/db-layer_pi7tix.jpg) +![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! +``` *** -## Third-Party Integrations Layer +## Example: Sync Products Once a Day -Third-party services and systems are integrated through Medusa's Commerce and Infrastructure Modules. You also create custom third-party integrations through a [custom module](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +In this section, you'll find a brief example of how you use a scheduled job to sync products to a third-party service. -Modules can be implemented within [plugins](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). +When implementing flows spanning across systems or [modules](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md), you use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). A workflow is a task made up of a series of steps, and you construct it like you would a regular function, but it's a special function that supports rollback mechanism in case of errors, background execution, and more. -### Commerce Modules +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: -[Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md) integrate third-party services relevant for commerce or user-facing features. For example, you can integrate [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe/index.html.md) through a Payment Module Provider, or [ShipStation](https://docs.medusajs.com/resources/integrations/guides/shipstation/index.html.md) through a Fulfillment Module Provider. +```ts title="src/jobs/sync-products.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncProductToErpWorkflow } from "../workflows/sync-products-to-erp" -You can also integrate third-party services for custom functionalities. For example, you can integrate [Sanity](https://docs.medusajs.com/resources/integrations/guides/sanity/index.html.md) for rich CMS capabilities, or [Odoo](https://docs.medusajs.com/resources/recipes/erp/odoo/index.html.md) to sync your Medusa application with your ERP system. +export default async function syncProductsJob(container: MedusaContainer) { + await syncProductToErpWorkflow(container) + .run() +} -You can replace any of the third-party services mentioned above to build your preferred commerce ecosystem. +export const config = { + name: "sync-products-job", + schedule: "0 0 * * *", +} +``` -![Diagram illustrating the Commerce Modules integration to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175357/Medusa%20Book/service-commerce_qcbdsl.jpg) +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. -### Infrastructure Modules +The next time you start the Medusa application, it will run this job every day at midnight. -[Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) integrate third-party services and systems that customize Medusa's infrastructure. Medusa has the following Infrastructure Modules: -- [Analytics Module](https://docs.medusajs.com/resources/infrastructure-modules/analytics/index.html.md): Tracks and analyzes user interactions and system events with third-party analytic providers. You can integrate [PostHog](https://docs.medusajs.com/resources/infrastructure-modules/analytics/posthog/index.html.md) as the analytics provider. -- [Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/index.html.md): Caches data that require heavy computation. You can integrate a custom module to handle the caching with services like Memcached, or use the existing [Redis Cache Module](https://docs.medusajs.com/resources/infrastructure-modules/cache/redis/index.html.md). -- [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md): A pub/sub system that allows you to subscribe to events and trigger them. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/event/redis/index.html.md) as the pub/sub system. -- [File Module](https://docs.medusajs.com/resources/infrastructure-modules/file/index.html.md): Manages file uploads and storage, such as upload of product images. You can integrate [AWS S3](https://docs.medusajs.com/resources/infrastructure-modules/file/s3/index.html.md) for file storage. -- [Locking Module](https://docs.medusajs.com/resources/infrastructure-modules/locking/index.html.md): Manages access to shared resources by multiple processes or threads, preventing conflict between processes and ensuring data consistency. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/locking/redis/index.html.md) for locking. -- [Notification Module](https://docs.medusajs.com/resources/infrastructure-modules/notification/index.html.md): Sends notifications to customers and users, such as for order updates or newsletters. You can integrate [SendGrid](https://docs.medusajs.com/resources/infrastructure-modules/notification/sendgrid/index.html.md) for sending emails. -- [Workflow Engine Module](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/index.html.md): Orchestrates workflows that hold the business logic of your application. You can integrate [Redis](https://docs.medusajs.com/resources/infrastructure-modules/workflow-engine/redis/index.html.md) to orchestrate workflows. +# Extend Core Commerce Features -All of the third-party services mentioned above can be replaced to help you build your preferred architecture and ecosystem. +In the upcoming chapters, you'll learn about the concepts and tools to extend Medusa's core commerce features. -![Diagram illustrating the Infrastructure Modules integration to third-party services and systems](https://res.cloudinary.com/dza7lstvk/image/upload/v1727175342/Medusa%20Book/service-arch_ozvryw.jpg) +In other commerce platforms, you extend core features and models through hacky workarounds that can introduce unexpected issues and side effects across the platform. It also makes your application difficult to maintain and upgrade in the long run. + +The Medusa Framework and orchestration tools mitigate these issues while supporting all your customization needs: + +- [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. +- [Workflow Hooks](https://docs.medusajs.com/learn/fundamentals/workflows/workflow-hooks/index.html.md): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. +- [Additional Data in API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. *** -## Full Diagram of Medusa's Architecture +## Next Chapters: Link Brands to Products Example -The following diagram illustrates Medusa's architecture including all its layers. +The next chapters explain how to use the tools mentioned above with step-by-step guides. You'll continue with the [brands example from the previous chapters](https://docs.medusajs.com/learn/customization/custom-features/index.html.md) to: -![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) - - -# 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. - -## What is Worker Mode? - -By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. - -In a production environment, you should deploy two separate instances of your Medusa application: - -1. A server instance that handles incoming requests to the application's API routes. -2. A worker instance that processes background tasks. This includes 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, as you'll see later in this chapter. - -This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. - -![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) - -*** - -## How to Set Worker Mode - -You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: - -- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. -- `worker`: run a worker process only. -- `server`: run the application server only. - -Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. - -For example, set the worker mode in `medusa-config.ts` to the following: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - projectConfig: { - workerMode: process.env.WORKER_MODE || "shared", - // ... - }, - // ... -}) -``` - -You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. - -Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: - -### Server Medusa Instance - -```bash -WORKER_MODE=server -``` - -### Worker Medusa Instance - -```bash -WORKER_MODE=worker -``` - -### Disable Admin in Worker Mode - -Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. - -To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - admin: { - disable: process.env.ADMIN_DISABLED === "true" || - false, - }, - // ... -}) -``` - -Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. - -Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: - -### Server Medusa Instance - -```bash -ADMIN_DISABLED=false -``` - -### Worker Medusa Instance - -```bash -ADMIN_DISABLED=true -``` +- Link brands from the custom [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to products from Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md). +- Extend the core product-creation workflow and the API route that uses it to allow setting the brand of a newly created product. +- Retrieve a product's associated brand's details. # Workflows @@ -4644,6 +4446,112 @@ 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. +# 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). + +After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: + +- Insert components, called [widgets](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md), on existing pages. +- Add new pages, called [UI Routes](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). + +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard + +*** + +## Next Chapters: View Brands in Dashboard + +In the next chapters, you'll continue with the brands example to: + +- Add a new section to the product details page that shows the product's brand. +- Add a new page in the dashboard that shows all brands in the store. + + +# Integrate Third-Party Systems + +Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. + +The Medusa Framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. + +In Medusa, you integrate a third-party system by: + +1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. +2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. +3. Executing the workflows you built in an [API route](https://docs.medusajs.com/learn/fundamentals/api-routes/index.html.md), at a scheduled time, or when an event is emitted. + +*** + +## Next Chapters: Sync Brands Example + +In the previous chapters, you've [added brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) to your Medusa application. In the next chapters, you will: + +1. Integrate a dummy third-party CMS in the Brand Module. +2. Sync brands to the CMS when a brand is created. +3. Sync brands from the CMS at a daily schedule. + + +# Customizations Next Steps: Learn the Fundamentals + +The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. + +The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. + +## Useful Guides + +The following guides and references are useful for your development journey: + +3. [Commerce Modules](https://docs.medusajs.com/resources/commerce-modules/index.html.md): Browse the list of Commerce Modules in Medusa and their references to learn how to use them. +4. [Service Factory Reference](https://docs.medusajs.com/resources/service-factory-reference/index.html.md): Learn about the methods generated by `MedusaService` with examples. +5. [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md): Browse the list of core workflows and their hooks that are useful for your customizations. +6. [Admin Injection Zones](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. + +*** + +## More Examples in Recipes + +In the [Recipes](https://docs.medusajs.com/resources/recipes/index.html.md) documentation, you'll also find step-by-step guides for different use cases, such as building a marketplace, digital products, and more. + + +# Re-Use Customizations with Plugins + +In the previous chapters, you've learned important concepts related to creating modules, implementing commerce features in workflows, exposing those features in API routes, customizing the Medusa Admin dashboard with Admin Extensions, and integrating third-party systems. + +You've implemented the brands example within a single Medusa application. However, this approach is not scalable when you want to reuse your customizations across multiple projects. + +To reuse your customizations across multiple Medusa applications, such as implementing brands in different projects, you can create a plugin. A plugin is an NPM package that encapsulates your customizations and can be installed in any Medusa application. Plugins can include modules, workflows, API routes, Admin Extensions, and more. + +![Diagram showcasing how the Brand Plugin would add its resources to any application it's installed in](https://res.cloudinary.com/dza7lstvk/image/upload/v1737540091/Medusa%20Book/brand-plugin_bk9zi9.jpg) + +Medusa provides the tooling to create a plugin package, test it in a local Medusa application, and publish it to NPM. + +To learn more about plugins and how to create them, refer to [this chapter](https://docs.medusajs.com/learn/fundamentals/plugins/index.html.md). + + # Usage Information At Medusa, we strive to provide the best experience for developers using our platform. For that reason, Medusa collects anonymous and non-sensitive data that provides a global understanding of how users are using Medusa. @@ -4734,6 +4642,98 @@ MEDUSA_FF_ANALYTICS=false ``` +# 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. + +## What is Worker Mode? + +By default, the Medusa application runs both the server, which handles all incoming requests, and the worker, which processes background tasks, in a single process. While this setup is suitable for development, it is not optimal for production environments where background tasks can be long-running or resource-intensive. + +In a production environment, you should deploy two separate instances of your Medusa application: + +1. A server instance that handles incoming requests to the application's API routes. +2. A worker instance that processes background tasks. This includes 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, as you'll see later in this chapter. + +This separation ensures that the server instance remains responsive to incoming requests, while the worker instance processes tasks in the background. + +![Diagram showcasing how the server and worker work together](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/Medusa%20Book/medusa-worker_klkbch.jpg?_a=BATFJtAA0) + +*** + +## How to Set Worker Mode + +You can set the worker mode of your application using the `projectConfig.workerMode` configuration in the `medusa-config.ts`. The `workerMode` configuration accepts the following values: + +- `shared`: (default) run the application in a single process, meaning the worker and server run in the same process. +- `worker`: run a worker process only. +- `server`: run the application server only. + +Instead of creating different projects with different worker mode configurations, you can set the worker mode using an environment variable. Then, the worker mode configuration will change based on the environment variable. + +For example, set the worker mode in `medusa-config.ts` to the following: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + workerMode: process.env.WORKER_MODE || "shared", + // ... + }, + // ... +}) +``` + +You set the worker mode configuration to the `process.env.WORKER_MODE` environment variable and set a default value of `shared`. + +Then, in the deployed server Medusa instance, set `WORKER_MODE` to `server`, and in the worker Medusa instance, set `WORKER_MODE` to `worker`: + +### Server Medusa Instance + +```bash +WORKER_MODE=server +``` + +### Worker Medusa Instance + +```bash +WORKER_MODE=worker +``` + +### Disable Admin in Worker Mode + +Since the worker instance only processes background tasks, you should disable the admin interface in it. That will save resources in the worker instance. + +To disable the admin interface, set the `admin.disable` configuration in the `medusa-config.ts` file: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + admin: { + disable: process.env.ADMIN_DISABLED === "true" || + false, + }, + // ... +}) +``` + +Similar to before, you set the value in an environment variable, allowing you to enable or disable the admin interface based on the environment. + +Then, in the deployed server Medusa instance, set `ADMIN_DISABLED` to `false`, and in the worker Medusa instance, set `ADMIN_DISABLED` to `true`: + +### Server Medusa Instance + +```bash +ADMIN_DISABLED=false +``` + +### Worker Medusa Instance + +```bash +ADMIN_DISABLED=true +``` + + # Write Integration Tests In this chapter, you'll learn about `medusaIntegrationTestRunner` from Medusa's Testing Framework and how to use it to write integration tests. @@ -4816,213 +4816,50 @@ 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 +# Admin Development Constraints -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. +This chapter lists some constraints of admin widgets and UI routes. -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. +## Arrow Functions -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. +Widget and UI route components must be created as arrow functions. -### 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 +```ts highlights={arrowHighlights} +// Don't +function ProductWidget() { + // ... } -export const POST = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const { result } = await createBrandWorkflow(req.scope) - .run({ - input: req.validatedBody, - }) - - res.json({ brand: result }) +// Do +const ProductWidget = () => { + // ... } ``` -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 +## Widget Zone -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. +A widget zone's value must be wrapped in double or single quotes. It can't be a template literal or a variable. -Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. +```ts highlights={zoneHighlights} +// Don't +export const config = defineWidgetConfig({ + zone: `product.details.before`, +}) -Learn more about API route validation in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/validation/index.html.md). +// Don't +const ZONE = "product.details.after" +export const config = defineWidgetConfig({ + zone: ZONE, +}) -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(), +// Do +export const config = defineWidgetConfig({ + zone: "product.details.before", }) ``` -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 @@ -5143,1839 +4980,6 @@ 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: 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(), -}) -``` - -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). - -*** - -## 3. Create Module Service - -You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. - -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/brand", - }, - ], -}) -``` - -The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). - -*** - -## 6. Generate and Run Migrations - -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 -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 Step: Create Brand Workflow - -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. - - -# 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 -} - -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. - - -# Create Brands UI Route in Admin - -In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination. - -### Prerequisites - -- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) - -## 1. Get Brands API Route - -In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. - -Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: - -```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} -// 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, - metadata: { count, take, skip } = {}, - } = await query.graph({ - entity: "brand", - ...req.queryConfig, - }) - - res.json({ - brands, - count, - limit: take, - offset: skip, - }) -} -``` - -In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. - -The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: - -- `fields`: The fields to retrieve in the brands. -- `limit`: The maximum number of items to retrieve. -- `offset`: The number of items to skip before retrieving the returned items. - -When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: - -- `count`: The total count of items. -- `take`: The maximum number of items returned in the `data` array. -- `skip`: The number of items skipped before retrieving the returned items. - -You return in the response the retrieved brands and the pagination configurations. - -Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). - -*** - -## 2. Add Default Query Configurations - -Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. - -Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object: - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformQuery, -} from "@medusajs/framework/http" -import { createFindParams } from "@medusajs/medusa/api/utils/validators" -// other imports... - -export const GetBrandsSchema = createFindParams() - -export default defineMiddlewares({ - routes: [ - // ... - { - matcher: "/admin/brands", - method: "GET", - middlewares: [ - validateAndTransformQuery( - GetBrandsSchema, - { - defaults: [ - "id", - "name", - "products.*", - ], - isList: true, - } - ), - ], - }, - - ], -}) -``` - -You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: - -- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: - - `fields`: A comma-separated string indicating the fields to retrieve. - - `limit`: The maximum number of items to retrieve. - - `offset`: The number of items to skip before retrieving the returned items. - - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) -- An object of Query configurations having the following properties: - - `defaults`: An array of default fields and relations to retrieve. - - `isList`: Whether the API route returns a list of items. - -By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route. - -Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). - -*** - -## 3. Initialize JS SDK - -In your custom UI route, you'll retrieve the brands 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 core API route. - -If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), 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). - -*** - -## 4. Add a UI Route to Show Brands - -You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard. - -Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). - -So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content: - -![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) - -```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights} -import { defineRouteConfig } from "@medusajs/admin-sdk" -import { TagSolid } from "@medusajs/icons" -import { - Container, -} from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../../lib/sdk" -import { useMemo, useState } from "react" - -const BrandsPage = () => { - // TODO retrieve brands - - return ( - - {/* TODO show brands */} - - ) -} - -export const config = defineRouteConfig({ - label: "Brands", - icon: TagSolid, -}) - -export default BrandsPage -``` - -A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK. - -So far, you only show a container. 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. - -### Retrieve Brands From API Route - -You'll now update the UI route to retrieve the brands from the API route you added earlier. - -First, add the following type in `src/admin/routes/brands/page.tsx`: - -```tsx title="src/admin/routes/brands/page.tsx" -type Brand = { - id: string - name: string -} -type BrandsResponse = { - brands: Brand[] - count: number - limit: number - offset: number -} -``` - -You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. - -To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`: - -```tsx title="src/admin/routes/brands/page.tsx" -import { - // ... - Heading, - createDataTableColumnHelper, - DataTable, - DataTablePaginationState, - useDataTable, -} from "@medusajs/ui" -``` - -You import the `DataTable` component and the following utilities: - -- `createDataTableColumnHelper`: A utility to create columns for the data table. -- `DataTablePaginationState`: A type that holds the pagination state of the data table. -- `useDataTable`: A hook to initialize and configure the data table. - -You also import the `Heading` component to show a heading above the data table. - -Next, you'll define the table's columns. Add the following before the `BrandsPage` component: - -```tsx title="src/admin/routes/brands/page.tsx" -const columnHelper = createDataTableColumnHelper() - -const columns = [ - columnHelper.accessor("id", { - header: "ID", - }), - columnHelper.accessor("name", { - header: "Name", - }), -] -``` - -You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. - -Then, replace the `// TODO retrieve brands` in the component with the following: - -```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} -const limit = 15 -const [pagination, setPagination] = useState({ - pageSize: limit, - pageIndex: 0, -}) -const offset = useMemo(() => { - return pagination.pageIndex * limit -}, [pagination]) - -const { data, isLoading } = useQuery({ - queryFn: () => sdk.client.fetch(`/admin/brands`, { - query: { - limit, - offset, - }, - }), - queryKey: [["brands", limit, offset]], -}) - -// TODO configure data table -``` - -To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties: - -- `pageSize`: The maximum number of items per page. You set it to `15`. -- `pageIndex`: A zero-based index of the current page of items. - -You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. - -Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. - -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. - -In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property. - -This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page. - -### Display Brands Table - -Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: - -```tsx title="src/admin/routes/brands/page.tsx" -const table = useDataTable({ - columns, - data: data?.brands || [], - getRowId: (row) => row.id, - rowCount: data?.count || 0, - isLoading, - pagination: { - state: pagination, - onPaginationChange: setPagination, - }, -}) -``` - -You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: - -- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility. -- `data`: The brands to display in the table. -- `getRowId`: A function that returns a unique identifier for a row. -- `rowCount`: The total count of items. This is used to determine the number of pages. -- `isLoading`: A boolean indicating whether the data is loading. -- `pagination`: An object to configure pagination. It accepts the following properties: - - `state`: The pagination state of the data table. - - `onPaginationChange`: A function to update the pagination state. - -Then, replace the `{/* TODO show brands */}` in the return statement with the following: - -```tsx title="src/admin/routes/brands/page.tsx" - - - Brands - - - - -``` - -This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook. - -*** - -## Test it Out - -To test out the UI route, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. - -![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) - -*** - -## Summary - -By following the previous chapters, you: - -- Injected a widget into the product details page to show the product's brand. -- Created a UI route in the Medusa Admin that shows the list of brands. - -*** - -## Next Steps: Integrate Third-Party Systems - -Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. - -In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. - - -# Guide: Add Product's Brand Widget in Admin - -In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. - -### Prerequisites - -- [Brands linked to products](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -## 1. Initialize JS SDK - -In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) that simplifies sending requests to the server's API routes. - -So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: - -![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) - -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: import.meta.env.VITE_BACKEND_URL || "/", - debug: import.meta.env.DEV, - auth: { - type: "session", - }, -}) -``` - -You initialize the SDK passing it the following options: - -- `baseUrl`: The URL to the Medusa server. -- `debug`: Whether to enable logging debug messages. This should only be enabled in development. -- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. - -Notice that you use `import.meta.env` to access environment variables in your customizations because the Medusa Admin is built on top of Vite. Learn more in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md). - -You can now use the SDK to send requests to the Medusa server. - -Learn more about the JS SDK and its options in [this reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). - -*** - -## 2. Add Widget to Product Details Page - -You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. - -Learn more about widgets in [this documentation](https://docs.medusajs.com/learn/fundamentals/admin/widgets/index.html.md). - -To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: - -![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) - -```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" -import { clx, Container, Heading, Text } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/sdk" - -type AdminProductBrand = AdminProduct & { - brand?: { - id: string - name: string - } -} - -const ProductBrandWidget = ({ - data: product, -}: DetailWidgetProps) => { - const { data: queryResult } = useQuery({ - queryFn: () => sdk.admin.product.retrieve(product.id, { - fields: "+brand.*", - }), - queryKey: [["product", product.id]], - }) - const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - - return ( - -
-
- Brand -
-
-
- - Name - - - - {brandName || "-"} - -
-
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductBrandWidget -``` - -A widget's file must export: - -- A React component to be rendered in the specified injection zone. The component must be the file's default export. -- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. - -Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. - -In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](https://docs.medusajs.com/api/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](https://docs.medusajs.com/ui/index.html.md) to maintain a consistent user interface and design in the dashboard. - -*** - -## Test it Out - -To test out your widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. - -![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) - -*** - -## Admin Components Guides - -When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. - -The [Admin Components guides](https://docs.medusajs.com/resources/admin-components/index.html.md) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. - -*** - -## Next Chapter: Add UI Route for Brands - -In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. - - -# Guide: Define Module Link Between Brand and Product - -In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md), and a product defined in the [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md) that's available in your Medusa application out-of-the-box. - -Modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from Commerce Modules with custom properties. To do that, you define module links. - -A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. - -In this chapter, you'll define a module link between the `Brand` data model of the Brand Module, and the `Product` data model of the Product Module. In later chapters, you'll manage and retrieve linked product and brand records. - -Learn more about module links in [this chapters](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). - -### Prerequisites - -- [Brand Module having a Brand data model](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) - -## 1. Define Link - -Links are defined in a TypeScript or JavaScript file under the `src/links` directory. The file defines and exports the link using `defineLink` from the Modules SDK. - -So, to define a link between the `Product` and `Brand` models, create the file `src/links/product-brand.ts` with the following content: - -![The directory structure of the Medusa application after adding the link.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733329897/Medusa%20Book/brands-link-dir-overview_t1rhlp.jpg) - -```ts title="src/links/product-brand.ts" highlights={highlights} -import BrandModule from "../modules/brand" -import ProductModule from "@medusajs/medusa/product" -import { defineLink } from "@medusajs/framework/utils" - -export default defineLink( - { - linkable: ProductModule.linkable.product, - isList: true, - }, - BrandModule.linkable.brand -) -``` - -You import each module's definition object from the `index.ts` file of the module's directory. Each module object has a special `linkable` property that holds the data models' link configurations. - -The `defineLink` function accepts two parameters of the same type, which is either: - -- The data model's link configuration, which you access from the Module's `linkable` property; -- Or an object that has two properties: - - `linkable`: the data model's link configuration, which you access from the Module's `linkable` property. - - `isList`: A boolean indicating whether many records of the data model can be linked to the other model. - -So, in the above code snippet, you define a link between the `Product` and `Brand` data models. Since a brand can be associated with multiple products, you enable `isList` in the `Product` model's object. - -*** - -## 2. Sync the Link to the Database - -A module link is represented in the database as a table that stores the IDs of linked records. So, after defining the link, run the following command to create the module link's table in the database: - -```bash -npx medusa db:migrate -``` - -This command reflects migrations on the database and syncs module links, which creates a table for the `product-brand` link. - -You can also run the `npx medusa db:sync-links` to just sync module links without running migrations. - -*** - -## Next Steps: Extend Create Product Flow - -In the next chapter, you'll extend Medusa's workflow and API route that create a product to allow associating a brand with a product. You'll also learn how to link brand and product records. - - -# Guide: Query Product's Brands - -In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. - -In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. - -### Prerequisites - -- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) -- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) - -*** - -## Approach 1: Retrieve Brands in Existing API Routes - -Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. - -Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. - -For example, send the following request to retrieve the list of products with their brands: - -```bash -curl 'http://localhost:9000/admin/products?fields=+brand.*' \ ---header 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -Any product that is linked to a brand will have a `brand` property in its object: - -```json title="Example Product Object" -{ - "id": "prod_123", - // ... - "brand": { - "id": "01JEB44M61BRM3ARM2RRMK7GJF", - "name": "Acme", - "created_at": "2024-12-05T09:59:08.737Z", - "updated_at": "2024-12-05T09:59:08.737Z", - "deleted_at": null - } -} -``` - -By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. - -### Limitations: Filtering by Brands in Existing API Routes - -While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. - -Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Approach 2: Use Query to Retrieve Linked Records - -You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. - -Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: - -```ts title="src/api/admin/brands/route.ts" highlights={highlights} -// other imports... -import { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - const query = req.scope.resolve("query") - - const { data: brands } = await query.graph({ - entity: "brand", - fields: ["*", "products.*"], - }) - - res.json({ brands }) -} -``` - -This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: - -- `entity`: The data model's name as specified in the first parameter of `model.define`. -- `fields`: An array of properties and relations to retrieve. You can pass: - - A property's name, such as `id`, or `*` for all properties. - - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. - -`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. - -### Test it Out - -To test the API route out, send a `GET` request to `/admin/brands`: - -```bash -curl 'http://localhost:9000/admin/brands' \ --H 'Authorization: Bearer {token}' -``` - -Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). - -This returns the brands in your store with their linked products. For example: - -```json title="Example Response" -{ - "brands": [ - { - "id": "123", - // ... - "products": [ - { - "id": "prod_123", - // ... - } - ] - } - ] -} -``` - -### Limitations: Filtering by Brand in Query - -While you can use Query to retrieve linked records, you can't filter by linked records. - -For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). - -*** - -## Summary - -By following the examples of the previous chapters, you: - -- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. -- Extended the create-product workflow and route to allow setting the product's brand while creating the product. -- Queried a product's brand, and vice versa. - -*** - -## Next Steps: Customize Medusa Admin - -Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. - -In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. - - -# Guide: Sync Brands from Medusa to Third-Party - -In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. - -In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. - -Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. - -Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. - -### Prerequisites - -- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) - -## 1. Emit Event in createBrandWorkflow - -Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. - -Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: - -```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} -// other imports... -import { - emitEventStep, -} from "@medusajs/medusa/core-flows" - -// ... - -export const createBrandWorkflow = createWorkflow( - "create-brand", - (input: CreateBrandInput) => { - // ... - - emitEventStep({ - eventName: "brand.created", - data: { - id: brand.id, - }, - }) - - return new WorkflowResponse(brand) - } -) -``` - -The `emitEventStep` accepts an object parameter having two properties: - -- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. -- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. - -You'll learn how to handle this event in a later step. - -*** - -## 2. Create Sync to Third-Party System Workflow - -The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -You'll create a `syncBrandToSystemWorkflow` that has two steps: - -- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. -- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. - -### syncBrandToCmsStep - -To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { Brand } from "../modules/brand/models/brand" -import { CMS_MODULE } from "../modules/cms" -import CmsModuleService from "../modules/cms/service" - -type SyncBrandToCmsStepInput = { - brand: InferTypeOf -} - -const syncBrandToCmsStep = createStep( - "sync-brand-to-cms", - async ({ brand }: SyncBrandToCmsStepInput, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.createBrand(brand) - - return new StepResponse(null, brand.id) - }, - async (id, { container }) => { - if (!id) { - return - } - - const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - - await cmsModuleService.deleteBrand(id) - } -) -``` - -You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. - -You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Create Workflow - -You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: - -```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} -// other imports... -import { - // ... - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -type SyncBrandToCmsWorkflowInput = { - id: string -} - -export const syncBrandToCmsWorkflow = createWorkflow( - "sync-brand-to-cms", - (input: SyncBrandToCmsWorkflowInput) => { - // @ts-ignore - const { data: brands } = useQueryGraphStep({ - entity: "brand", - fields: ["*"], - filters: { - id: input.id, - }, - options: { - throwIfKeyNotFound: true, - }, - }) - - syncBrandToCmsStep({ - brand: brands[0], - } as SyncBrandToCmsStepInput) - - return new WorkflowResponse({}) - } -) -``` - -You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: - -- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. -- `syncBrandToCmsStep`: Create the brand in the third-party CMS. - -You'll execute this workflow in the subscriber next. - -Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). - -*** - -## 3. Handle brand.created Event - -You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. - -Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: - -![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) - -```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} -import type { - SubscriberConfig, - SubscriberArgs, -} from "@medusajs/framework" -import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" - -export default async function brandCreatedHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncBrandToCmsWorkflow(container).run({ - input: data, - }) -} - -export const config: SubscriberConfig = { - event: "brand.created", -} -``` - -A subscriber file must export: - -- The asynchronous function that's executed when the event is emitted. This must be the file's default export. -- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. - -The subscriber function accepts an object parameter that has two properties: - -- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. -- `container`: The Medusa container used to resolve Framework and commerce tools. - -In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. - -Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). - -*** - -## Test it Out - -To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. - -First, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: - -```bash -curl -X POST 'http://localhost:9000/auth/user/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "admin@medusa-test.com", - "password": "supersecret" -}' -``` - -Make sure to replace the email and password with your admin user's credentials. - -Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). - -Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: - -```bash -curl -X POST 'http://localhost:9000/admin/brands' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data '{ - "name": "Acme" -}' -``` - -This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: - -```plain -info: Processing brand.created which has 1 subscribers -http: POST /admin/brands ← - (200) - 16.418 ms -info: Sending a POST request to /brands. -info: Request Data: { - "id": "01JEDWENYD361P664WRQPMC3J8", - "name": "Acme", - "created_at": "2024-12-06T11:42:32.909Z", - "updated_at": "2024-12-06T11:42:32.909Z", - "deleted_at": null -} -info: API Key: "123" -``` - -*** - -## Next Chapter: Sync Brand from Third-Party CMS to Medusa - -You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. - - -# Guide: 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: Schedule Syncing Brands from Third-Party - -In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. - -However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. - -You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. - -Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). - -In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. - -### Prerequisites - -- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) - -*** - -## 1. Implement Syncing Workflow - -You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. - -Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. - -Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). - -This workflow will have three steps: - -1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. -2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. -3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. - -### retrieveBrandsFromCmsStep - -To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: - -![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) - -```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import CmsModuleService from "../modules/cms/service" -import { CMS_MODULE } from "../modules/cms" - -const retrieveBrandsFromCmsStep = createStep( - "retrieve-brands-from-cms", - async (_, { container }) => { - const cmsModuleService: CmsModuleService = container.resolve( - CMS_MODULE - ) - - const brands = await cmsModuleService.retrieveBrands() - - return new StepResponse(brands) - } -) -``` - -You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. - -### createBrandsStep - -The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: - -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -// other imports... -import BrandModuleService from "../modules/brand/service" -import { BRAND_MODULE } from "../modules/brand" - -// ... - -type CreateBrand = { - name: string -} - -type CreateBrandsInput = { - brands: CreateBrand[] -} - -export const createBrandsStep = createStep( - "create-brands-step", - async (input: CreateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const brands = await brandModuleService.createBrands(input.brands) - - return new StepResponse(brands, brands) - }, - async (brands, { container }) => { - if (!brands) { - return - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) - } -) -``` - -The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. - -The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. - -Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). - -### Update Brands Step - -The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: - -```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} -// ... - -type UpdateBrand = { - id: string - name: string -} - -type UpdateBrandsInput = { - brands: UpdateBrand[] -} - -export const updateBrandsStep = createStep( - "update-brands-step", - async ({ brands }: UpdateBrandsInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - const prevUpdatedBrands = await brandModuleService.listBrands({ - id: brands.map((brand) => brand.id), - }) - - const updatedBrands = await brandModuleService.updateBrands(brands) - - return new StepResponse(updatedBrands, prevUpdatedBrands) - }, - async (prevUpdatedBrands, { container }) => { - if (!prevUpdatedBrands) { - return - } - - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) - - await brandModuleService.updateBrands(prevUpdatedBrands) - } -) -``` - -The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. - -In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. - -### Create Workflow - -Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -// other imports... -import { - // ... - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -// ... - -export const syncBrandsFromCmsWorkflow = createWorkflow( - "sync-brands-from-system", - () => { - const brands = retrieveBrandsFromCmsStep() - - // TODO create and update brands - } -) -``` - -In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. - -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. - -Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -So, replace the `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const { toCreate, toUpdate } = transform( - { - brands, - }, - (data) => { - const toCreate: CreateBrand[] = [] - const toUpdate: UpdateBrand[] = [] - - data.brands.forEach((brand) => { - if (brand.external_id) { - toUpdate.push({ - id: brand.external_id as string, - name: brand.name as string, - }) - } else { - toCreate.push({ - name: brand.name as string, - }) - } - }) - - return { toCreate, toUpdate } - } -) - -// TODO create and update the brands -``` - -`transform` accepts two parameters: - -1. The data to be passed to the function in the second parameter. -2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. - -In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. - -You now have the list of brands to create and update. So, replace the new `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-cms.ts" -const created = createBrandsStep({ brands: toCreate }) -const updated = updateBrandsStep({ brands: toUpdate }) - -return new WorkflowResponse({ - created, - updated, -}) -``` - -You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. - -Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. - -*** - -## 2. Schedule Syncing Task - -You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. - -A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: - -![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) - -```ts title="src/jobs/sync-brands-from-cms.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" - -export default async function (container: MedusaContainer) { - const logger = container.resolve("logger") - - const { result } = await syncBrandsFromCmsWorkflow(container).run() - - logger.info( - `Synced brands from third-party system: ${ - result.created.length - } brands created and ${result.updated.length} brands updated.`) -} - -export const config = { - name: "sync-brands-from-system", - schedule: "0 0 * * *", // change to * * * * * for debugging -} -``` - -A scheduled job file must export: - -- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. -- An object of scheduled jobs configuration. It has two properties: - - `name`: A unique name for the scheduled job. - - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - -The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. - -Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. - -*** - -## Test it Out - -To test out the scheduled job, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. - -*** - -## Summary - -By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems. - -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. - - # Environment Variables in Admin Customizations In this chapter, you'll learn how to use environment variables in your admin customizations. @@ -7090,51 +5094,6 @@ export default ProductWidget ``` -# 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 Routing Customizations The Medusa Admin dashboard uses [React Router](https://reactrouter.com) under the hood to manage routing. So, you can have more flexibility in routing-related customizations using some of React Router's utilities, hooks, and components. @@ -7288,279 +5247,46 @@ 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. -# Guide: Integrate Third-Party Brand System +# Admin Widgets -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 more about widgets and how to use them. -Learn more about modules in [this chapter](https://docs.medusajs.com/learn/fundamentals/modules/index.html.md). +## What is an Admin Widget? -## 1. Create Module Directory +The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. -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) +For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. *** -## 2. Create Module Service +## How to Create a Widget? -Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. +### Prerequisites -Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: +- [Medusa application installed](https://docs.medusajs.com/learn/installation/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 widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. -```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} -import { Logger, ConfigModule } from "@medusajs/framework/types" +For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: -export type ModuleOptions = { - apiKey: string -} +![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) -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, -}) -``` - -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`. - -*** - -## 4. Add Module to Medusa's Configurations - -Finally, add the module to the Medusa configurations at `medusa-config.ts`: - -```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 -CMS_API_KEY=123 -``` - -*** - -## Next Steps: Sync Brand From Medusa to CMS - -You can now use the CMS Module's service to perform actions on the third-party CMS. - -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. - - -# Admin Development Tips - -In this chapter, you'll find some tips for your admin development. - -## Send Requests to API Routes - -To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. - -Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. - -First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: - -### Medusa Project - -```ts title="src/admin/lib/config.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", - }, -}) -``` - -### Medusa Plugin - -```ts title="src/admin/lib/config.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: __BACKEND_URL__ || "/", - auth: { - type: "session", - }, -}) -``` - -Notice that you use `import.meta.env` in a Medusa project to access environment variables in your customizations, whereas in a plugin you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter. - -Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). - -Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. - -For example: - -### Query - -```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useQuery } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" +import { Container, Heading } from "@medusajs/ui" +// The widget const ProductWidget = () => { - const { data, isLoading } = useQuery({ - queryFn: () => sdk.admin.product.list(), - queryKey: ["products"], - }) - return ( - {isLoading && Loading...} - {data?.products && ( -
    - {data.products.map((product) => ( -
  • {product.title}
  • - ))} -
- )} -
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.list.before", -}) - -export default ProductWidget -``` - -### Mutation - -```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Button, Container } from "@medusajs/ui" -import { useMutation } from "@tanstack/react-query" -import { sdk } from "../lib/config" -import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" - -const ProductWidget = ({ - data: productData, -}: DetailWidgetProps) => { - const { mutateAsync } = useMutation({ - mutationFn: (payload: HttpTypes.AdminUpdateProduct) => - sdk.admin.product.update(productData.id, payload), - onSuccess: () => alert("updated product"), - }) - - const handleUpdate = () => { - mutateAsync({ - title: "New Product Title", - }) - } - - return ( - - +
+ Product Widget +
) } +// The widget's configurations export const config = defineWidgetConfig({ zone: "product.details.before", }) @@ -7568,29 +5294,76 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). +You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. -### Use Route Loaders for Initial Data +To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. -You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). +In the example above, the widget is injected at the top of a product’s details. + +The widget component must be created as an arrow function. + +### Test the Widget + +To test out the widget, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open a product’s details page. You’ll find your custom widget at the top of the page. *** -## Global Variables in Admin Customizations +## Props Passed in Detail Pages -In your admin customizations, you can use the following global variables: +Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. -- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. -- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. -- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. +For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: + +```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container, Heading } from "@medusajs/ui" +import { + DetailWidgetProps, + AdminProduct, +} from "@medusajs/framework/types" + +// The widget +const ProductWidget = ({ + data, +}: DetailWidgetProps) => { + return ( + +
+ + Product Widget {data.title} + +
+
+ ) +} + +// The widget's configurations +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. *** -## Admin Translations +## Injection Zone -The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. +Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. -Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). +*** + +## Admin Components List + +To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. # Admin UI Routes @@ -7829,230 +5602,1429 @@ To build admin customizations that match the Medusa Admin's designs and layouts, For more customizations related to routes, refer to the [Routing Customizations chapter](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). -# Admin Widgets +# Seed Data with Custom CLI Script -In this chapter, you’ll learn more about widgets and how to use them. +In this chapter, you'll learn how to seed data using a custom CLI script. -## What is an Admin Widget? +## How to Seed Data -The Medusa Admin dashboard's pages are customizable to insert widgets of custom content in pre-defined injection zones. You create these widgets as React components that allow admin users to perform custom actions. +To seed dummy data for development or demo purposes, use a custom CLI script. -For example, you can add a widget on the product details page that allow admin users to sync products to a third-party service. +In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. -*** +### Example: Seed Dummy Products -## How to Create a Widget? +In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. -### Prerequisites - -- [Medusa application installed](https://docs.medusajs.com/learn/installation/index.html.md) - -You create a widget in a `.tsx` file under the `src/admin/widgets` directory. The file’s default export must be the widget, which is the React component that renders the custom content. The file must also export the widget’s configurations indicating where to insert the widget. - -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - -![Example of widget file in the application's directory structure](https://res.cloudinary.com/dza7lstvk/image/upload/v1732867137/Medusa%20Book/widget-dir-overview_dqsbct.jpg) - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={widgetHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -// The widget -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} - -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -You export the `ProductWidget` component, which shows the heading `Product Widget`. In the widget, you use [Medusa UI](https://docs.medusajs.com/ui/index.html.md), a package that Medusa maintains to allow you to customize the dashboard with the same components used to build it. - -To export the widget's configurations, you use `defineWidgetConfig` from the Admin Extension SDK. It accepts as a parameter an object with the `zone` property, whose value is a string or an array of strings, each being the name of the zone to inject the widget into. - -In the example above, the widget is injected at the top of a product’s details. - -The widget component must be created as an arrow function. - -### Test the Widget - -To test out the widget, start the Medusa application: +First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: ```bash npm2yarn -npm run dev +npm install --save-dev @faker-js/faker ``` -Then, open a product’s details page. You’ll find your custom widget at the top of the page. +Then, create the file `src/scripts/demo-products.ts` with the following content: -*** - -## Props Passed in Detail Pages - -Widgets that are injected into a details page receive a `data` prop, which is the main data of the details page. - -For example, a widget injected into the `product.details.before` zone receives the product's details in the `data` prop: - -```tsx title="src/admin/widgets/product-widget.tsx" highlights={detailHighlights} -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" +```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" +import { ExecArgs } from "@medusajs/framework/types" +import { faker } from "@faker-js/faker" import { - DetailWidgetProps, - AdminProduct, -} from "@medusajs/framework/types" + ContainerRegistrationKeys, + Modules, + ProductStatus, +} from "@medusajs/framework/utils" +import { + createInventoryLevelsWorkflow, + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" -// The widget -const ProductWidget = ({ - data, -}: DetailWidgetProps) => { - return ( - -
- - Product Widget {data.title} - -
-
+export default async function seedDummyProducts({ + container, +}: ExecArgs) { + const salesChannelModuleService = container.resolve( + Modules.SALES_CHANNEL + ) + const logger = container.resolve( + ContainerRegistrationKeys.LOGGER + ) + const query = container.resolve( + ContainerRegistrationKeys.QUERY ) -} -// The widget's configurations -export const config = defineWidgetConfig({ - zone: "product.details.before", + const defaultSalesChannel = await salesChannelModuleService + .listSalesChannels({ + name: "Default Sales Channel", + }) + + const sizeOptions = ["S", "M", "L", "XL"] + const colorOptions = ["Black", "White"] + const currency_code = "eur" + const productsNum = 50 + + // TODO seed products +} +``` + +So far, in the script, you: + +- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. +- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. +- Initialize some default data to use when seeding the products next. + +Next, replace the `TODO` with the following: + +```ts title="src/scripts/demo-products.ts" +const productsData = new Array(productsNum).fill(0).map((_, index) => { + const title = faker.commerce.product() + "_" + index + return { + title, + is_giftcard: true, + description: faker.commerce.productDescription(), + status: ProductStatus.PUBLISHED, + options: [ + { + title: "Size", + values: sizeOptions, + }, + { + title: "Color", + values: colorOptions, + }, + ], + images: [ + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + { + url: faker.image.urlPlaceholder({ + text: title, + }), + }, + ], + variants: new Array(10).fill(0).map((_, variantIndex) => ({ + title: `${title} ${variantIndex}`, + sku: `variant-${variantIndex}${index}`, + prices: new Array(10).fill(0).map((_, priceIndex) => ({ + currency_code, + amount: 10 * priceIndex, + })), + options: { + Size: sizeOptions[Math.floor(Math.random() * 3)], + }, + })), + shipping_profile_id: "sp_123", + sales_channels: [ + { + id: defaultSalesChannel[0].id, + }, + ], + } }) -export default ProductWidget +// TODO seed products ``` -The props type is `DetailWidgetProps`, and it accepts as a type argument the expected type of `data`. For the product details page, it's `AdminProduct`. +You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. -*** +Then, replace the new `TODO` with the following: -## Injection Zone +```ts title="src/scripts/demo-products.ts" +const { result: products } = await createProductsWorkflow(container).run({ + input: { + products: productsData, + }, +}) -Refer to [this reference](https://docs.medusajs.com/resources/admin-widget-injection-zones/index.html.md) for the full list of injection zones and their props. +logger.info(`Seeded ${products.length} products.`) -*** - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](https://docs.medusajs.com/resources/admin-components/index.html.md) to find common components. - - -# 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." - ) - } - - // ... -} +// TODO add inventory levels ``` -The `MedusaError` class accepts in its constructor two parameters: +You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. -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. +Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: -### Error Object in Response +```ts title="src/scripts/demo-products.ts" +logger.info("Seeding inventory levels.") -The error object returned in the response has two properties: +const { data: stockLocations } = await query.graph({ + entity: "stock_location", + fields: ["id"], +}) -- `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. +const { data: inventoryItems } = await query.graph({ + entity: "inventory_item", + fields: ["id"], +}) -### MedusaError Types +const inventoryLevels = inventoryItems.map((inventoryItem) => ({ + location_id: stockLocations[0].id, + stocked_quantity: 1000000, + inventory_item_id: inventoryItem.id, +})) -|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\`| +await createInventoryLevelsWorkflow(container).run({ + input: { + inventory_levels: inventoryLevels, + }, +}) -*** +logger.info("Finished seeding inventory levels data.") +``` -## Override Error Handler +You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. -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. +Then, you generate inventory levels for each inventory item, associating it with the first stock location. -This error handler will also be used for errors thrown in Medusa's API routes and resources. +Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. -For example, create `src/api/middlewares.ts` with the following: +### Test Script -```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" +To test out the script, run the following command in your project's directory: -export default defineMiddlewares({ - errorHandler: ( - error: MedusaError | any, - req: MedusaRequest, - res: MedusaResponse, - next: MedusaNextFunction - ) => { - res.status(400).json({ - error: "Something happened.", - }) +```bash +npx medusa exec ./src/scripts/demo-products.ts +``` + +This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. + + +# Admin Development Tips + +In this chapter, you'll find some tips for your admin development. + +## Send Requests to API Routes + +To send a request to an API route in the Medusa Application, use Medusa's [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) with [Tanstack Query](https://tanstack.com/query/latest). Both of these tools are installed in your project by default. + +Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install `v5.64.2` as a development dependency. + +First, create the file `src/admin/lib/config.ts` to setup the SDK for use in your customizations: + +### Medusa Project + +```ts title="src/admin/lib/config.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", }, }) ``` -The `errorHandler` property's value is a function that accepts four parameters: +### Medusa Plugin -1. The error thrown. Its type can be `MedusaError` or any other thrown error type. -2. A request object of type `MedusaRequest`. -3. A response object of type `MedusaResponse`. -4. A function of type MedusaNextFunction that executes the next middleware in the stack. +```ts title="src/admin/lib/config.ts" +import Medusa from "@medusajs/js-sdk" -This example overrides Medusa's default error handler with a handler that always returns a `400` status code with the same message. +export const sdk = new Medusa({ + baseUrl: __BACKEND_URL__ || "/", + auth: { + type: "session", + }, +}) +``` + +Notice that you use `import.meta.env` in a Medusa project to access environment variables in your customizations, whereas in a plugin you use the global variable `__BACKEND_URL__` to access the backend URL. You can learn more in the [Admin Environment Variables](https://docs.medusajs.com/learn/fundamentals/admin/environment-variables/index.html.md) chapter. + +Learn more about the JS SDK's configurations [this documentation](https://docs.medusajs.com/resources/js-sdk#js-sdk-configurations/index.html.md). + +Then, use the configured SDK with the `useQuery` Tanstack Query hook to send `GET` requests, and `useMutation` hook to send `POST` or `DELETE` requests. + +For example: + +### Query + +```tsx title="src/admin/widgets/product-widget.ts" highlights={queryHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = () => { + const { data, isLoading } = useQuery({ + queryFn: () => sdk.admin.product.list(), + queryKey: ["products"], + }) + + return ( + + {isLoading && Loading...} + {data?.products && ( +
    + {data.products.map((product) => ( +
  • {product.title}
  • + ))} +
+ )} +
+ ) +} + +export const config = defineWidgetConfig({ + zone: "product.list.before", +}) + +export default ProductWidget +``` + +### Mutation + +```tsx title="src/admin/widgets/product-widget.ts" highlights={mutationHighlights} +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Button, Container } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../lib/config" +import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types" + +const ProductWidget = ({ + data: productData, +}: DetailWidgetProps) => { + const { mutateAsync } = useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateProduct) => + sdk.admin.product.update(productData.id, payload), + onSuccess: () => alert("updated product"), + }) + + const handleUpdate = () => { + mutateAsync({ + title: "New Product Title", + }) + } + + return ( + + + + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +You can also send requests to custom routes as explained in the [JS SDK reference](https://docs.medusajs.com/resources/js-sdk/index.html.md). + +### Use Route Loaders for Initial Data + +You may need to retrieve data before your component is rendered, or you may need to pass some initial data to your component to be used while data is being fetched. In those cases, you can use a [route loader](https://docs.medusajs.com/learn/fundamentals/admin/routing/index.html.md). + +*** + +## Global Variables in Admin Customizations + +In your admin customizations, you can use the following global variables: + +- `__BASE__`: The base path of the Medusa Admin, as set in the [admin.path](https://docs.medusajs.com/learn/configurations/medusa-config#path/index.html.md) configuration in `medusa-config.ts`. +- `__BACKEND_URL__`: The URL to the Medusa backend, as set in the [admin.backendUrl](https://docs.medusajs.com/learn/configurations/medusa-config#backendurl/index.html.md) configuration in `medusa-config.ts`. +- `__STOREFRONT_URL__`: The URL to the storefront, as set in the [admin.storefrontUrl](https://docs.medusajs.com/learn/configurations/medusa-config#storefrontUrl/index.html.md) configuration in `medusa-config.ts`. + +*** + +## Admin Translations + +The Medusa Admin dashboard can be displayed in languages other than English, which is the default. Other languages are added through community contributions. + +Learn how to add a new language translation for the Medusa Admin in [this guide](https://docs.medusajs.com/learn/resources/contribution-guidelines/admin-translations/index.html.md). + + +# Data Model Database Index + +In this chapter, you’ll learn how to define a database index on a data model. + +You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md). + +## Define Database Index on Data Model + +A data model has an `indexes` method that defines database indices on its properties. + +The index can be on multiple columns (composite index). For example: + +```ts highlights={dataModelIndexHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + }, +]) + +export default MyCustom +``` + +The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. + +In the above example, you define a composite index on the `name` and `age` properties. + +### Index Conditions + +An index can have conditions. For example: + +```ts highlights={conditionHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: 30, + }, + }, +]) + +export default MyCustom +``` + +The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. + +In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. + +A property's condition can be a negation. For example: + +```ts highlights={negationHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number().nullable(), +}).indexes([ + { + on: ["name", "age"], + where: { + age: { + $ne: null, + }, + }, + }, +]) + +export default MyCustom +``` + +A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. + +In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. + +### Unique Database Index + +The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. + +For example: + +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" + +const MyCustom = model.define("my_custom", { + id: model.id().primaryKey(), + name: model.text(), + age: model.number(), +}).indexes([ + { + on: ["name", "age"], + unique: true, + }, +]) + +export default MyCustom +``` + +This creates a unique composite index on the `name` and `age` properties. + + +# Add Data Model Check Constraints + +In this chapter, you'll learn how to add check constraints to your data model. + +## What is a Check Constraint? + +A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. + +For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. + +*** + +## How to Set a Check Constraint? + +To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. + +For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: + +```ts highlights={checks1Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + (columns) => `${columns.price} >= 0`, +]) +``` + +The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. + +The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. + +You can also pass an object to the `checks` method: + +```ts highlights={checks2Highlights} +import { model } from "@medusajs/framework/utils" + +const CustomProduct = model.define("custom_product", { + // ... + price: model.bigNumber(), +}) +.checks([ + { + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, +]) +``` + +The object accepts the following properties: + +- `name`: The check constraint's name. +- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). + +*** + +## Apply in Migrations + +After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. + +To generate a migration for the data model's module then reflect it on the database, run the following command: + +```bash +npx medusa db:generate custom_module +npx medusa db:migrate +``` + +The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. + + +# Infer Type of Data Model + +In this chapter, you'll learn how to infer the type of a data model. + +## How to Infer Type of Data Model? + +Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. + +Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. + +For example: + +```ts +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../modules/blog/models/post" // relative path to the model + +export type Post = InferTypeOf +``` + +`InferTypeOf` accepts as a type argument the type of the data model. + +Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. + +You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: + +```ts title="Example Service" +// other imports... +import { InferTypeOf } from "@medusajs/framework/types" +import { Post } from "../models/post" + +type Post = InferTypeOf + +class BlogModuleService extends MedusaService({ Post }) { + async doSomething(): Promise { + // ... + } +} +``` + + +# Manage Relationships + +In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. + +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 Properties + +In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties. + +## Data Model's Default Properties + +By default, Medusa creates the following properties for every data model: + +- `created_at`: A [dateTime](#dateTime) property that stores when a record of the data model was created. +- `updated_at`: A [dateTime](#dateTime) property that stores when a record of the data model was updated. +- `deleted_at`: A [dateTime](#dateTime) property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date. + +*** + +## Property Types + +This section covers the different property types you can define in a data model's schema using the `model` methods. + +### id + +The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers. + +For example: + +```ts highlights={idHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id(), + // ... +}) + +export default Post +``` + +### text + +The `text` method defines a string property. + +For example: + +```ts highlights={textHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + name: model.text(), + // ... +}) + +export default Post +``` + +### number + +The `number` method defines a number property. + +For example: + +```ts highlights={numberHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + age: model.number(), + // ... +}) + +export default Post +``` + +### float + +This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). + +The `float` method defines a number property that allows for values with decimal places. + +Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). + +For example: + +```ts highlights={floatHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + rating: model.float(), + // ... +}) + +export default Post +``` + +### bigNumber + +The `bigNumber` method defines a number property that expects large numbers, such as prices. + +Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). + +For example: + +```ts highlights={bigNumberHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + price: model.bigNumber(), + // ... +}) + +export default Post +``` + +### boolean + +The `boolean` method defines a boolean property. + +For example: + +```ts highlights={booleanHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + hasAccount: model.boolean(), + // ... +}) + +export default Post +``` + +### enum + +The `enum` method defines a property whose value can only be one of the specified values. + +For example: + +```ts highlights={enumHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + color: model.enum(["black", "white"]), + // ... +}) + +export default Post +``` + +The `enum` method accepts an array of possible string values. + +### dateTime + +The `dateTime` method defines a timestamp property. + +For example: + +```ts highlights={dateTimeHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + date_of_birth: model.dateTime(), + // ... +}) + +export default Post +``` + +### json + +The `json` method defines a property whose value is a stringified JSON object. + +For example: + +```ts highlights={jsonHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + metadata: model.json(), + // ... +}) + +export default Post +``` + +### array + +The `array` method defines an array of strings property. + +For example: + +```ts highlights={arrHightlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + names: model.array(), + // ... +}) + +export default Post +``` + +### Properties Reference + +Refer to the [Data Model Language (DML) reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. + +*** + +## Set Primary Key Property + +To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. + +For example: + +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + // ... +}) + +export default Post +``` + +In the example above, the `id` property is defined as the data model's primary key. + +*** + +## Property Default Value + +Use the `default` method on a property's definition to specify the default value of a property. + +For example: + +```ts highlights={defaultHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + color: model + .enum(["black", "white"]) + .default("black"), + age: model + .number() + .default(0), + // ... +}) + +export default Post +``` + +In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. + +*** + +## Make Property Optional + +Use the `nullable` method to indicate that a property’s value can be `null`. This is useful when you want a property to be optional. + +For example: + +```ts highlights={nullableHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + price: model.bigNumber().nullable(), + // ... +}) + +export default Post +``` + +In the example above, the `price` property is configured to allow `null` values, making it optional. + +*** + +## Unique Property + +The `unique` method indicates that a property’s value must be unique in the database through a unique index. + +For example: + +```ts highlights={uniqueHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + email: model.text().unique(), + // ... +}) + +export default User +``` + +In this example, multiple users can’t have the same email. + +*** + +## Define Database Index on Property + +Use the `index` method on a property's definition to define a database index. + +For example: + +```ts highlights={dbIndexHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + id: model.id().primaryKey(), + name: model.text().index( + "IDX_MY_CUSTOM_NAME" + ), +}) + +export default Post +``` + +The `index` method optionally accepts the name of the index as a parameter. + +In this example, you define an index on the `name` property. + +*** + +## Define a Searchable Property + +Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. + +When the `q` filter is passed, the data model's searchable properties are queried to find matching records. + +Use the `searchable` method on a `text` property to indicate that it's searchable. + +For example: + +```ts highlights={searchableHighlights} +import { model } from "@medusajs/framework/utils" + +const Post = model.define("post", { + title: model.text().searchable(), + // ... +}) + +export default Post +``` + +In this example, the `title` property is searchable. + +### Search Example + +If you pass a `q` filter to the `listPosts` method: + +```ts +const posts = await blogModuleService.listPosts({ + q: "New Products", +}) +``` + +This retrieves records that include `New Products` in their `title` property. + + +# Data Model Relationships + +In this chapter, you’ll learn how to define relationships between data models in your module. + +## What is a Relationship Property? + +A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. + +When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. + +You want to create a relation between data models in the same module. + +You want to create a relationship between data models in different modules. Use module links instead. + +*** + +## One-to-One Relationship + +A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. + +To define a one-to-one relationship, create relationship properties in the data models using the following methods: + +1. `hasOne`: indicates that the model has one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. + +For example: + +```ts highlights={oneToOneHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In the example above, a user has one email, and an email belongs to one user. + +The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. + +The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. + +### Optional Relationship + +To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). + +### One-sided One-to-One Relationship + +If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. + +For example: + +```ts highlights={oneToOneUndefinedHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + user: model.belongsTo(() => User, { + mappedBy: undefined, + }), +}) +``` + +### One-to-One Relationship in the Database + +When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) + +*** + +## One-to-Many Relationship + +A one-to-many relationship indicates that one record of a data model has many records of another data model. + +To define a one-to-many relationship, create relationship properties in the data models using the following methods: + +1. `hasMany`: indicates that the model has more than one record of the specified model. +2. `belongsTo`: indicates that the model belongs to one record of the specified model. + +For example: + +```ts highlights={oneToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +In this example, a store has many products, but a product belongs to one store. + +### Optional Relationship + +To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). + +### One-to-Many Relationship in the Database + +When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: + +1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. +2. A foreign key on the `{relation_name}_id` column to the table of the related data model. + +![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) + +*** + +## Many-to-Many Relationship + +A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. + +To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. + +For example: + +```ts highlights={manyToManyHighlights} +import { model } from "@medusajs/framework/utils" + +const Order = model.define("order", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + mappedBy: "orders", + pivotTable: "order_product", + joinColumn: "order_id", + inverseJoinColumn: "product_id", + }), +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order, { + mappedBy: "products", + }), +}) +``` + +The `manyToMany` method accepts two parameters: + +1. A function that returns the associated data model. +2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: + - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. + - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. + - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. + - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. + +The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). + +Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. + +In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. + +### Many-to-Many Relationship in the Database + +When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. + +The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables. + +The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: + +- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; +- Or the inferred name `{table_name}_id`. + +![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) + +### Many-To-Many with Custom Columns + +To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. + +For example: + +```ts highlights={manyToManyColumnHighlights} +import { model } from "@medusajs/framework/utils" + +export const Order = model.define("order_test", { + id: model.id().primaryKey(), + products: model.manyToMany(() => Product, { + pivotEntity: () => OrderProduct, + }), +}) + +export const Product = model.define("product_test", { + id: model.id().primaryKey(), + orders: model.manyToMany(() => Order), +}) + +export const OrderProduct = model.define("orders_products", { + id: model.id().primaryKey(), + order: model.belongsTo(() => Order, { + mappedBy: "products", + }), + product: model.belongsTo(() => Product, { + mappedBy: "orders", + }), + metadata: model.json().nullable(), +}) +``` + +The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. + +The `OrderProduct` model defines, aside from the ID, the following properties: + +- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. +- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. +- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. + +*** + +## Set Relationship Name in the Other Model + +The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. + +This is useful if the relationship property’s name is different from that of the associated data model. + +As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. + +For example: + +```ts highlights={relationNameHighlights} +import { model } from "@medusajs/framework/utils" + +const User = model.define("user", { + id: model.id().primaryKey(), + email: model.hasOne(() => Email, { + mappedBy: "owner", + }), +}) + +const Email = model.define("email", { + id: model.id().primaryKey(), + owner: model.belongsTo(() => User, { + mappedBy: "email", + }), +}) +``` + +In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. + +*** + +## Cascades + +When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. + +For example, if a store is deleted, its products should also be deleted. + +The `cascades` method used on a data model configures which child records an operation is cascaded to. + +For example: + +```ts highlights={highlights} +import { model } from "@medusajs/framework/utils" + +const Store = model.define("store", { + id: model.id().primaryKey(), + products: model.hasMany(() => Product), +}) +.cascades({ + delete: ["products"], +}) + +const Product = model.define("product", { + id: model.id().primaryKey(), + store: model.belongsTo(() => Store, { + mappedBy: "products", + }), +}) +``` + +The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. + +In the example above, when a store is deleted, its associated products are also deleted. # Pass Additional Data to Medusa's API Route @@ -8254,6 +7226,104 @@ createProductsWorkflow.hooks.productsCreated( This updates the products to their original state before adding the brand to their `metadata` property. +# Migrations + +In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. + +## What is a Migration? + +A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. + +The migration's file has a class with two methods: + +- The `up` method reflects changes on the database. +- The `down` method reverts the changes made in the `up` method. + +*** + +## Generate Migration + +Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. + +For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: + +```bash +npx medusa db:generate blog +``` + +This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). + +*** + +## Write a Migration Manually + +You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. + +For example: + +```ts title="src/modules/blog/migrations/Migration202507021059.ts" +import { Migration } from "@mikro-orm/migrations" + +export class Migration202507021059 extends Migration { + + async up(): Promise { + this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") + } + + async down(): Promise { + this.addSql("drop table if exists \"author\" cascade;") + } + +} +``` + +The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. + +In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. + +Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. + +*** + +## Run the Migration + +To run your migration, run the following command: + +This command also syncs module links. If you don't want that, use the `--skip-links` option. + +```bash +npx medusa db:migrate +``` + +This reflects the changes in the database as implemented in the migration's `up` method. + +*** + +## Rollback the Migration + +To rollback or revert the last migration you ran for a module, run the following command: + +```bash +npx medusa db:rollback blog +``` + +This rolls back the last ran migration on the Blog Module. + +### Caution: Rollback Migration before Deleting + +If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. + +For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. + +So, always rollback the migration before deleting it. + +*** + +## More Database Commands + +To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). + + # Handling CORS in API Routes In this chapter, you’ll learn about the CORS middleware and how to configure it for custom API routes. @@ -8366,6 +7436,113 @@ export default defineMiddlewares({ This retrieves the configurations exported from `medusa-config.ts` and applies the `storeCors` to routes starting with `/custom`. +# 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. @@ -8409,228 +7586,6 @@ This adds two API Routes: - A `POST` route at `http://localhost:9000/hello-world`. -# Override API Routes - -In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa. - -## Approaches to Consider Before Overriding API Routes - -While building customizations in your Medusa application, you may need to make changes to existing API routes for your business use case. - -Medusa provides the following approaches to customize API routes: - -|Approach|Description| -|---|---| -|Pass Additional Data|Pass custom data to the API route with custom validation.| -|Perform Custom Logic within an Existing Flows|API routes execute workflows to perform business logic, which may have hooks that allow you to perform custom logic.| -|Use Custom Middlewares|Use custom middlewares to perform custom logic before the API route is executed. However, you cannot remove or replace middlewares applied to existing API routes.| -|Listen to Events in Subscribers|Functionalities in API routes may trigger events that you can handle in subscribers. This is useful if you're performing an action that isn't integral to the API route's core functionality or response.| - -If the above approaches do not meet your needs, you can consider the approaches mentioned in the rest of this chapter. - -*** - -## Replicate, Don't Override API Routes - -If the approaches mentioned in the [section above](#approaches-to-consider-before-overriding-api-routes) do not meet your needs, you can replicate an existing API route and modify it to suit your requirements. - -By replicating instead of overriding, the original API route remains intact, allowing you to easily revert to the original functionality if needed. You can also update your Medusa version without worrying about breaking changes in the original API route. - -*** - -## How to Replicate an API Route? - -Medusa's API routes are generally slim and use logic contained in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). So, creating a custom route based on the original route is straightforward. - -You can view the source code for Medusa's API routes in the [Medusa GitHub repository](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/api). - -For example, if you need to allow vendors to access the `POST /admin/products` API route, you can create an API route in your Medusa project at `src/api/vendor/products/route.ts` with the [same code as the original route](https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/admin/products/route.ts#L88). Then, you can make changes to it or its middlewares. - -*** - -## When to Replicate an API Route? - -Some examples of when you might want to replicate an API route include: - -|Use Case|Description| -|---|---| -|Custom Validation|You want to change the validation logic for a specific API route, and the | -|Change Authentication|You want to remove required authentication for a specific API route, or you want to allow custom | -|Custom Response|You want to change the response format of an existing API route.| -|Override Middleware|You want to override the middleware applied on existing API routes. Because of | - - -# 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. - - # Middlewares In this chapter, you’ll learn about middlewares and how to create them. @@ -9156,6 +8111,228 @@ 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). +# Override API Routes + +In this chapter, you'll learn the approach recommended when you need to override an existing API route in Medusa. + +## Approaches to Consider Before Overriding API Routes + +While building customizations in your Medusa application, you may need to make changes to existing API routes for your business use case. + +Medusa provides the following approaches to customize API routes: + +|Approach|Description| +|---|---| +|Pass Additional Data|Pass custom data to the API route with custom validation.| +|Perform Custom Logic within an Existing Flows|API routes execute workflows to perform business logic, which may have hooks that allow you to perform custom logic.| +|Use Custom Middlewares|Use custom middlewares to perform custom logic before the API route is executed. However, you cannot remove or replace middlewares applied to existing API routes.| +|Listen to Events in Subscribers|Functionalities in API routes may trigger events that you can handle in subscribers. This is useful if you're performing an action that isn't integral to the API route's core functionality or response.| + +If the above approaches do not meet your needs, you can consider the approaches mentioned in the rest of this chapter. + +*** + +## Replicate, Don't Override API Routes + +If the approaches mentioned in the [section above](#approaches-to-consider-before-overriding-api-routes) do not meet your needs, you can replicate an existing API route and modify it to suit your requirements. + +By replicating instead of overriding, the original API route remains intact, allowing you to easily revert to the original functionality if needed. You can also update your Medusa version without worrying about breaking changes in the original API route. + +*** + +## How to Replicate an API Route? + +Medusa's API routes are generally slim and use logic contained in [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). So, creating a custom route based on the original route is straightforward. + +You can view the source code for Medusa's API routes in the [Medusa GitHub repository](https://github.com/medusajs/medusa/tree/develop/packages/medusa/src/api). + +For example, if you need to allow vendors to access the `POST /admin/products` API route, you can create an API route in your Medusa project at `src/api/vendor/products/route.ts` with the [same code as the original route](https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/admin/products/route.ts#L88). Then, you can make changes to it or its middlewares. + +*** + +## When to Replicate an API Route? + +Some examples of when you might want to replicate an API route include: + +|Use Case|Description| +|---|---| +|Custom Validation|You want to change the validation logic for a specific API route, and the | +|Change Authentication|You want to remove required authentication for a specific API route, or you want to allow custom | +|Custom Response|You want to change the response format of an existing API route.| +|Override Middleware|You want to override the middleware applied on existing API routes. Because of | + + +# 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. @@ -9421,108 +8598,6 @@ 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. -# API Route Response - -In this chapter, you'll learn how to send a response in your API route. - -## Send a JSON Response - -To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. - -For example: - -```ts title="src/api/custom/route.ts" highlights={jsonHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, World!", - }) -} -``` - -This API route returns the following JSON object: - -```json -{ - "message": "Hello, World!" -} -``` - -*** - -## Set Response Status Code - -By default, setting the JSON data using the `json` method returns a response with a `200` status code. - -To change the status code, use the `status` method of the `MedusaResponse` object. - -For example: - -```ts title="src/api/custom/route.ts" highlights={statusHighlight} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.status(201).json({ - message: "Hello, World!", - }) -} -``` - -The response of this API route has the status code `201`. - -*** - -## Change Response Content Type - -To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. - -For example, to create an API route that returns an event stream: - -```ts highlights={streamHighlights} -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" - -export const GET = async ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.writeHead(200, { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - Connection: "keep-alive", - }) - - const interval = setInterval(() => { - res.write("Streaming data...\n") - }, 3000) - - req.on("end", () => { - clearInterval(interval) - res.end() - }) -} -``` - -The `writeHead` method accepts two parameters: - -1. The first one is the response's status code. -2. The second is an object of key-value pairs to set the headers of the response. - -This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. - -*** - -## Do More with Responses - -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. @@ -9854,6 +8929,108 @@ For example, if you omit the `a` parameter, you'll receive a `400` response code To see different examples and learn more about creating a validation schema, refer to [Zod's documentation](https://zod.dev). +# API Route Response + +In this chapter, you'll learn how to send a response in your API route. + +## Send a JSON Response + +To send a JSON response, use the `json` method of the `MedusaResponse` object passed as the second parameter of your API route handler. + +For example: + +```ts title="src/api/custom/route.ts" highlights={jsonHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.json({ + message: "Hello, World!", + }) +} +``` + +This API route returns the following JSON object: + +```json +{ + "message": "Hello, World!" +} +``` + +*** + +## Set Response Status Code + +By default, setting the JSON data using the `json` method returns a response with a `200` status code. + +To change the status code, use the `status` method of the `MedusaResponse` object. + +For example: + +```ts title="src/api/custom/route.ts" highlights={statusHighlight} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.status(201).json({ + message: "Hello, World!", + }) +} +``` + +The response of this API route has the status code `201`. + +*** + +## Change Response Content Type + +To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. + +For example, to create an API route that returns an event stream: + +```ts highlights={streamHighlights} +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("Streaming data...\n") + }, 3000) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` + +The `writeHead` method accepts two parameters: + +1. The first one is the response's status code. +2. The second is an object of key-value pairs to set the headers of the response. + +This API route opens a stream by setting the `Content-Type` in the header to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. + +*** + +## Do More with Responses + +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. + + # Event Data Payload In this chapter, you'll learn how subscribers receive an event's data payload. @@ -9899,194 +9076,6 @@ This logs the product ID received in the `product.created` event’s data payloa Refer to [this reference](!resources!/references/events) for a full list of events emitted by Medusa and their data payloads. */} -# Seed Data with Custom CLI Script - -In this chapter, you'll learn how to seed data using a custom CLI script. - -## How to Seed Data - -To seed dummy data for development or demo purposes, use a custom CLI script. - -In the CLI script, use your custom workflows or Medusa's existing workflows, which you can browse in [this reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md), to seed data. - -### Example: Seed Dummy Products - -In this section, you'll follow an example of creating a custom CLI script that seeds fifty dummy products. - -First, install the [Faker](https://fakerjs.dev/) library to generate random data in your script: - -```bash npm2yarn -npm install --save-dev @faker-js/faker -``` - -Then, create the file `src/scripts/demo-products.ts` with the following content: - -```ts title="src/scripts/demo-products.ts" highlights={highlights} collapsibleLines="1-12" expandButtonLabel="Show Imports" -import { ExecArgs } from "@medusajs/framework/types" -import { faker } from "@faker-js/faker" -import { - ContainerRegistrationKeys, - Modules, - ProductStatus, -} from "@medusajs/framework/utils" -import { - createInventoryLevelsWorkflow, - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -export default async function seedDummyProducts({ - container, -}: ExecArgs) { - const salesChannelModuleService = container.resolve( - Modules.SALES_CHANNEL - ) - const logger = container.resolve( - ContainerRegistrationKeys.LOGGER - ) - const query = container.resolve( - ContainerRegistrationKeys.QUERY - ) - - const defaultSalesChannel = await salesChannelModuleService - .listSalesChannels({ - name: "Default Sales Channel", - }) - - const sizeOptions = ["S", "M", "L", "XL"] - const colorOptions = ["Black", "White"] - const currency_code = "eur" - const productsNum = 50 - - // TODO seed products -} -``` - -So far, in the script, you: - -- Resolve the Sales Channel Module's main service to retrieve the application's default sales channel. This is the sales channel the dummy products will be available in. -- Resolve the Logger to log messages in the terminal, and Query to later retrieve data useful for the seeded products. -- Initialize some default data to use when seeding the products next. - -Next, replace the `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const productsData = new Array(productsNum).fill(0).map((_, index) => { - const title = faker.commerce.product() + "_" + index - return { - title, - is_giftcard: true, - description: faker.commerce.productDescription(), - status: ProductStatus.PUBLISHED, - options: [ - { - title: "Size", - values: sizeOptions, - }, - { - title: "Color", - values: colorOptions, - }, - ], - images: [ - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - { - url: faker.image.urlPlaceholder({ - text: title, - }), - }, - ], - variants: new Array(10).fill(0).map((_, variantIndex) => ({ - title: `${title} ${variantIndex}`, - sku: `variant-${variantIndex}${index}`, - prices: new Array(10).fill(0).map((_, priceIndex) => ({ - currency_code, - amount: 10 * priceIndex, - })), - options: { - Size: sizeOptions[Math.floor(Math.random() * 3)], - }, - })), - shipping_profile_id: "sp_123", - sales_channels: [ - { - id: defaultSalesChannel[0].id, - }, - ], - } -}) - -// TODO seed products -``` - -You generate fifty products using the sales channel and variables you initialized, and using Faker for random data, such as the product's title or images. - -Then, replace the new `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -const { result: products } = await createProductsWorkflow(container).run({ - input: { - products: productsData, - }, -}) - -logger.info(`Seeded ${products.length} products.`) - -// TODO add inventory levels -``` - -You create the generated products using the `createProductsWorkflow` imported previously from `@medusajs/medusa/core-flows`. It accepts the product data as input, and returns the created products. - -Only thing left is to create inventory levels for the products. So, replace the last `TODO` with the following: - -```ts title="src/scripts/demo-products.ts" -logger.info("Seeding inventory levels.") - -const { data: stockLocations } = await query.graph({ - entity: "stock_location", - fields: ["id"], -}) - -const { data: inventoryItems } = await query.graph({ - entity: "inventory_item", - fields: ["id"], -}) - -const inventoryLevels = inventoryItems.map((inventoryItem) => ({ - location_id: stockLocations[0].id, - stocked_quantity: 1000000, - inventory_item_id: inventoryItem.id, -})) - -await createInventoryLevelsWorkflow(container).run({ - input: { - inventory_levels: inventoryLevels, - }, -}) - -logger.info("Finished seeding inventory levels data.") -``` - -You use Query to retrieve the stock location, to use the first location in the application, and the inventory items. - -Then, you generate inventory levels for each inventory item, associating it with the first stock location. - -Finally, you use the `createInventoryLevelsWorkflow` from Medusa's core workflows to create the inventory levels. - -### Test Script - -To test out the script, run the following command in your project's directory: - -```bash -npx medusa exec ./src/scripts/demo-products.ts -``` - -This seeds the products to your database. If you run your Medusa application and view the products in the dashboard, you'll find fifty new products. - - # 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. @@ -10255,1195 +9244,6 @@ If you execute the `performAction` method of your service, the event is emitted Any subscribers listening to the event are also executed. -# Add Data Model Check Constraints - -In this chapter, you'll learn how to add check constraints to your data model. - -## What is a Check Constraint? - -A check constraint is a condition that must be satisfied by records inserted into a database table, otherwise an error is thrown. - -For example, if you have a data model with a `price` property, you want to only allow positive number values. So, you add a check constraint that fails when inserting a record with a negative price value. - -*** - -## How to Set a Check Constraint? - -To set check constraints on a data model, use the `checks` method. This method accepts an array of check constraints to apply on the data model. - -For example, to set a check constraint on a `price` property that ensures its value can only be a positive number: - -```ts highlights={checks1Highlights} -import { model } from "@medusajs/framework/utils" - -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - (columns) => `${columns.price} >= 0`, -]) -``` - -The item passed in the array parameter of `checks` can be a callback function that accepts as a parameter an object whose keys are the names of the properties in the data model schema, and values the respective column name in the database. - -The function returns a string indicating the [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). In the expression, use the `columns` parameter to access a property's column name. - -You can also pass an object to the `checks` method: - -```ts highlights={checks2Highlights} -import { model } from "@medusajs/framework/utils" - -const CustomProduct = model.define("custom_product", { - // ... - price: model.bigNumber(), -}) -.checks([ - { - name: "custom_product_price_check", - expression: (columns) => `${columns.price} >= 0`, - }, -]) -``` - -The object accepts the following properties: - -- `name`: The check constraint's name. -- `expression`: A function similar to the one that can be passed to the array. It accepts an object of columns and returns an [SQL check constraint expression](https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-CHECK-CONSTRAINTS). - -*** - -## Apply in Migrations - -After adding the check constraint, make sure to generate and run migrations if you already have the table in the database. Otherwise, the check constraint won't be reflected. - -To generate a migration for the data model's module then reflect it on the database, run the following command: - -```bash -npx medusa db:generate custom_module -npx medusa db:migrate -``` - -The first command generates the migration under the `migrations` directory of your module's directory, and the second reflects it on the database. - - -# Data Model Database Index - -In this chapter, you’ll learn how to define a database index on a data model. - -You can also define an index on a property as explained in the [Properties chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#define-database-index-on-property/index.html.md). - -## Define Database Index on Data Model - -A data model has an `indexes` method that defines database indices on its properties. - -The index can be on multiple columns (composite index). For example: - -```ts highlights={dataModelIndexHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - }, -]) - -export default MyCustom -``` - -The `indexes` method receives an array of indices as a parameter. Each index is an object with a required `on` property indicating the properties to apply the index on. - -In the above example, you define a composite index on the `name` and `age` properties. - -### Index Conditions - -An index can have conditions. For example: - -```ts highlights={conditionHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: 30, - }, - }, -]) - -export default MyCustom -``` - -The index object passed to `indexes` accepts a `where` property whose value is an object of conditions. The object's key is a property's name, and its value is the condition on that property. - -In the example above, the composite index is created on the `name` and `age` properties when the `age`'s value is `30`. - -A property's condition can be a negation. For example: - -```ts highlights={negationHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number().nullable(), -}).indexes([ - { - on: ["name", "age"], - where: { - age: { - $ne: null, - }, - }, - }, -]) - -export default MyCustom -``` - -A property's value in `where` can be an object having a `$ne` property. `$ne`'s value indicates what the specified property's value shouldn't be. - -In the example above, the composite index is created on the `name` and `age` properties when `age`'s value is not `null`. - -### Unique Database Index - -The object passed to `indexes` accepts a `unique` property indicating that the created index must be a unique index. - -For example: - -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" - -const MyCustom = model.define("my_custom", { - id: model.id().primaryKey(), - name: model.text(), - age: model.number(), -}).indexes([ - { - on: ["name", "age"], - unique: true, - }, -]) - -export default MyCustom -``` - -This creates a unique composite index on the `name` and `age` properties. - - -# Infer Type of Data Model - -In this chapter, you'll learn how to infer the type of a data model. - -## How to Infer Type of Data Model? - -Consider you have a `Post` data model. You can't reference this data model in a type, such as a workflow input or service method output types, since it's a variable. - -Instead, Medusa provides `InferTypeOf` that transforms your data model to a type. - -For example: - -```ts -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../modules/blog/models/post" // relative path to the model - -export type Post = InferTypeOf -``` - -`InferTypeOf` accepts as a type argument the type of the data model. - -Since the `Post` data model is a variable, use the `typeof` operator to pass the data model as a type argument to `InferTypeOf`. - -You can now use the `Post` type to reference a data model in other types, such as in workflow inputs or service method outputs: - -```ts title="Example Service" -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Post } from "../models/post" - -type Post = InferTypeOf - -class BlogModuleService extends MedusaService({ Post }) { - async doSomething(): Promise { - // ... - } -} -``` - - -# Manage Relationships - -In this chapter, you'll learn how to manage relationships between data models when creating, updating, or retrieving records using the module's main service. - -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. - -## What is a Relationship Property? - -A relationship property defines an association in the database between two models. It's created using the Data Model Language (DML) methods, such as `hasOne` or `belongsTo`. - -When you generate a migration for these data models, the migrations include foreign key columns or pivot tables, based on the relationship's type. - -You want to create a relation between data models in the same module. - -You want to create a relationship between data models in different modules. Use module links instead. - -*** - -## One-to-One Relationship - -A one-to-one relationship indicates that one record of a data model belongs to or is associated with another. - -To define a one-to-one relationship, create relationship properties in the data models using the following methods: - -1. `hasOne`: indicates that the model has one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. - -For example: - -```ts highlights={oneToOneHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In the example above, a user has one email, and an email belongs to one user. - -The `hasOne` and `belongsTo` methods accept a function as the first parameter. The function returns the associated data model. - -The `belongsTo` method also requires passing as a second parameter an object with the property `mappedBy`. Its value is the name of the relationship property in the other data model. - -### Optional Relationship - -To make the relationship optional on the `hasOne` or `belongsTo` side, use the `nullable` method on either property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). - -### One-sided One-to-One Relationship - -If the one-to-one relationship is only defined on one side, pass `undefined` to the `mappedBy` property in the `belongsTo` method. - -For example: - -```ts highlights={oneToOneUndefinedHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - user: model.belongsTo(() => User, { - mappedBy: undefined, - }), -}) -``` - -### One-to-One Relationship in the Database - -When you generate the migrations of data models that have a one-to-one relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `email` table will have a `user_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. - -![Diagram illustrating the relation between user and email records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733492/Medusa%20Book/one-to-one_cj5np3.jpg) - -*** - -## One-to-Many Relationship - -A one-to-many relationship indicates that one record of a data model has many records of another data model. - -To define a one-to-many relationship, create relationship properties in the data models using the following methods: - -1. `hasMany`: indicates that the model has more than one record of the specified model. -2. `belongsTo`: indicates that the model belongs to one record of the specified model. - -For example: - -```ts highlights={oneToManyHighlights} -import { model } from "@medusajs/framework/utils" - -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) -``` - -In this example, a store has many products, but a product belongs to one store. - -### Optional Relationship - -To make the relationship optional on the `belongsTo` side, use the `nullable` method on the property as explained in [this chapter](https://docs.medusajs.com/learn/fundamentals/data-models/properties#make-property-optional/index.html.md). - -### One-to-Many Relationship in the Database - -When you generate the migrations of data models that have a one-to-many relationship, the migration adds to the table of the data model that has the `belongsTo` property: - -1. A column of the format `{relation_name}_id` to store the ID of the record of the related data model. For example, the `product` table will have a `store_id` column. -2. A foreign key on the `{relation_name}_id` column to the table of the related data model. - -![Diagram illustrating the relation between a store and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726733937/Medusa%20Book/one-to-many_d6wtcw.jpg) - -*** - -## Many-to-Many Relationship - -A many-to-many relationship indicates that many records of a data model can be associated with many records of another data model. - -To define a many-to-many relationship, create relationship properties in the data models using the `manyToMany` method. - -For example: - -```ts highlights={manyToManyHighlights} -import { model } from "@medusajs/framework/utils" - -const Order = model.define("order", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - mappedBy: "orders", - pivotTable: "order_product", - joinColumn: "order_id", - inverseJoinColumn: "product_id", - }), -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order, { - mappedBy: "products", - }), -}) -``` - -The `manyToMany` method accepts two parameters: - -1. A function that returns the associated data model. -2. An object of optional configuration. Only one of the data models in the relation can define the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations, and it's considered the owner data model. The object can accept the following properties: - - `mappedBy`: The name of the relationship property in the other data model. If not set, the property's name is inferred from the associated data model's name. - - `pivotTable`: The name of the pivot table created in the database for the many-to-many relation. If not set, the pivot table is inferred by combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - - `joinColumn`: The name of the column in the pivot table that points to the owner model's primary key. - - `inverseJoinColumn`: The name of the column in the pivot table that points to the owned model's primary key. - -The `pivotTable`, `joinColumn`, and `inverseJoinColumn` properties are only available after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). - -Following [Medusa v2.1.0](https://github.com/medusajs/medusa/releases/tag/v2.1.0), if `pivotTable`, `joinColumn`, and `inverseJoinColumn` aren't specified on either model, the owner is decided based on alphabetical order. So, in the example above, the `Order` data model would be the owner. - -In this example, an order is associated with many products, and a product is associated with many orders. Since the `pivotTable`, `joinColumn`, and `inverseJoinColumn` configurations are defined on the order, it's considered the owner data model. - -### Many-to-Many Relationship in the Database - -When you generate the migrations of data models that have a many-to-many relationship, the migration adds a new pivot table. Its name is either the name you specify in the `pivotTable` configuration or the inferred name combining the names of the data models' tables in alphabetical order, separating them by `_`, and pluralizing the last name. For example, `order_products`. - -The pivot table has a column with the name `{data_model}_id` for each of the data model's tables. It also has foreign keys on each of these columns to their respective tables. - -The pivot table has columns with foreign keys pointing to the primary key of the associated tables. The column's name is either: - -- The value of the `joinColumn` configuration for the owner table, and the `inverseJoinColumn` configuration for the owned table; -- Or the inferred name `{table_name}_id`. - -![Diagram illustrating the relation between order and product records in the database](https://res.cloudinary.com/dza7lstvk/image/upload/v1726734269/Medusa%20Book/many-to-many_fzy5pq.jpg) - -### Many-To-Many with Custom Columns - -To add custom columns to the pivot table between two data models having a many-to-many relationship, you must define a new data model that represents the pivot table. - -For example: - -```ts highlights={manyToManyColumnHighlights} -import { model } from "@medusajs/framework/utils" - -export const Order = model.define("order_test", { - id: model.id().primaryKey(), - products: model.manyToMany(() => Product, { - pivotEntity: () => OrderProduct, - }), -}) - -export const Product = model.define("product_test", { - id: model.id().primaryKey(), - orders: model.manyToMany(() => Order), -}) - -export const OrderProduct = model.define("orders_products", { - id: model.id().primaryKey(), - order: model.belongsTo(() => Order, { - mappedBy: "products", - }), - product: model.belongsTo(() => Product, { - mappedBy: "orders", - }), - metadata: model.json().nullable(), -}) -``` - -The `Order` and `Product` data models have a many-to-many relationship. To add extra columns to the created pivot table, you pass a `pivotEntity` option to the `products` relation in `Order` (since `Order` is the owner). The value of `pivotEntity` is a function that returns the data model representing the pivot table. - -The `OrderProduct` model defines, aside from the ID, the following properties: - -- `order`: A relation that indicates this model belongs to the `Order` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Order` data model. -- `product`: A relation that indicates this model belongs to the `Product` data model. You set the `mappedBy` option to the many-to-many relation's name in the `Product` data model. -- `metadata`: An extra column to add to the pivot table of type `json`. You can add other columns as well to the model. - -*** - -## Set Relationship Name in the Other Model - -The relationship property methods accept as a second parameter an object of options. The `mappedBy` property defines the name of the relationship in the other data model. - -This is useful if the relationship property’s name is different from that of the associated data model. - -As seen in previous examples, the `mappedBy` option is required for the `belongsTo` method. - -For example: - -```ts highlights={relationNameHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - id: model.id().primaryKey(), - email: model.hasOne(() => Email, { - mappedBy: "owner", - }), -}) - -const Email = model.define("email", { - id: model.id().primaryKey(), - owner: model.belongsTo(() => User, { - mappedBy: "email", - }), -}) -``` - -In this example, you specify in the `User` data model’s relationship property that the name of the relationship in the `Email` data model is `owner`. - -*** - -## Cascades - -When an operation is performed on a data model, such as record deletion, the relationship cascade specifies what related data model records should be affected by it. - -For example, if a store is deleted, its products should also be deleted. - -The `cascades` method used on a data model configures which child records an operation is cascaded to. - -For example: - -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" - -const Store = model.define("store", { - id: model.id().primaryKey(), - products: model.hasMany(() => Product), -}) -.cascades({ - delete: ["products"], -}) - -const Product = model.define("product", { - id: model.id().primaryKey(), - store: model.belongsTo(() => Store, { - mappedBy: "products", - }), -}) -``` - -The `cascades` method accepts an object. Its key is the operation’s name, such as `delete`. The value is an array of relationship property names that the operation is cascaded to. - -In the example above, when a store is deleted, its associated products are also deleted. - - -# Migrations - -In this chapter, you'll learn what a migration is and how to generate a migration or write it manually. - -## What is a Migration? - -A migration is a TypeScript or JavaScript file that defines database changes made by a module. Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations. - -The migration's file has a class with two methods: - -- The `up` method reflects changes on the database. -- The `down` method reverts the changes made in the `up` method. - -*** - -## Generate Migration - -Instead of you writing the migration manually, the Medusa CLI tool provides a [db:generate](https://docs.medusajs.com/resources/medusa-cli/commands/db#dbgenerate/index.html.md) command to generate a migration for a modules' data models. - -For example, assuming you have a `blog` Module, you can generate a migration for it by running the following command: - -```bash -npx medusa db:generate blog -``` - -This generates a migration file under the `migrations` directory of the Blog Module. You can then run it to reflect the changes in the database as mentioned in [this section](#run-the-migration). - -*** - -## Write a Migration Manually - -You can also write migrations manually. To do that, create a file in the `migrations` directory of the module and in it, a class that has an `up` and `down` method. The class's name should be of the format `Migration{YEAR}{MONTH}{DAY}{HOUR}{MINUTE}.ts` to ensure migrations are ran in the correct order. - -For example: - -```ts title="src/modules/blog/migrations/Migration202507021059.ts" -import { Migration } from "@mikro-orm/migrations" - -export class Migration202507021059 extends Migration { - - async up(): Promise { - this.addSql("create table if not exists \"author\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"author_pkey\" primary key (\"id\"));") - } - - async down(): Promise { - this.addSql("drop table if exists \"author\" cascade;") - } - -} -``` - -The migration class in the file extends the `Migration` class imported from `@mikro-orm/migrations`. In the `up` and `down` method of the migration class, you use the `addSql` method provided by MikroORM's `Migration` class to run PostgreSQL syntax. - -In the example above, the `up` method creates the table `author`, and the `down` method drops the table if the migration is reverted. - -Refer to [MikroORM's documentation](https://mikro-orm.io/docs/migrations#migration-class) for more details on writing migrations. - -*** - -## Run the Migration - -To run your migration, run the following command: - -This command also syncs module links. If you don't want that, use the `--skip-links` option. - -```bash -npx medusa db:migrate -``` - -This reflects the changes in the database as implemented in the migration's `up` method. - -*** - -## Rollback the Migration - -To rollback or revert the last migration you ran for a module, run the following command: - -```bash -npx medusa db:rollback blog -``` - -This rolls back the last ran migration on the Blog Module. - -### Caution: Rollback Migration before Deleting - -If you need to delete a migration file, make sure to rollback the migration first. Otherwise, you might encounter issues when generating and running new migrations. - -For example, if you delete the migration of the Blog Module, then try to create a new one, Medusa will create a brand new migration that re-creates the tables or indices. If those are still in the database, you might encounter errors. - -So, always rollback the migration before deleting it. - -*** - -## More Database Commands - -To learn more about the Medusa CLI's database commands, refer to [this CLI reference](https://docs.medusajs.com/resources/medusa-cli/commands/db/index.html.md). - - -# Data Model Properties - -In this chapter, you'll learn about the different property types you can use in a data model and how to configure a data model's properties. - -## Data Model's Default Properties - -By default, Medusa creates the following properties for every data model: - -- `created_at`: A [dateTime](#dateTime) property that stores when a record of the data model was created. -- `updated_at`: A [dateTime](#dateTime) property that stores when a record of the data model was updated. -- `deleted_at`: A [dateTime](#dateTime) property that stores when a record of the data model was deleted. When you soft-delete a record, Medusa sets the `deleted_at` property to the current date. - -*** - -## Property Types - -This section covers the different property types you can define in a data model's schema using the `model` methods. - -### id - -The `id` method defines an automatically generated string ID property. The generated ID is a unique string that has a mix of letters and numbers. - -For example: - -```ts highlights={idHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id(), - // ... -}) - -export default Post -``` - -### text - -The `text` method defines a string property. - -For example: - -```ts highlights={textHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - name: model.text(), - // ... -}) - -export default Post -``` - -### number - -The `number` method defines a number property. - -For example: - -```ts highlights={numberHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - age: model.number(), - // ... -}) - -export default Post -``` - -### float - -This property is only available after [Medusa v2.1.2](https://github.com/medusajs/medusa/releases/tag/v2.1.2). - -The `float` method defines a number property that allows for values with decimal places. - -Use this property type when it's less important to have high precision for numbers with large decimal places. Alternatively, for higher percision, use the [bigNumber property](#bignumber). - -For example: - -```ts highlights={floatHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - rating: model.float(), - // ... -}) - -export default Post -``` - -### bigNumber - -The `bigNumber` method defines a number property that expects large numbers, such as prices. - -Use this property type when it's important to have high precision for numbers with large decimal places. Alternatively, for less percision, use the [float property](#float). - -For example: - -```ts highlights={bigNumberHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - price: model.bigNumber(), - // ... -}) - -export default Post -``` - -### boolean - -The `boolean` method defines a boolean property. - -For example: - -```ts highlights={booleanHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - hasAccount: model.boolean(), - // ... -}) - -export default Post -``` - -### enum - -The `enum` method defines a property whose value can only be one of the specified values. - -For example: - -```ts highlights={enumHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - color: model.enum(["black", "white"]), - // ... -}) - -export default Post -``` - -The `enum` method accepts an array of possible string values. - -### dateTime - -The `dateTime` method defines a timestamp property. - -For example: - -```ts highlights={dateTimeHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - date_of_birth: model.dateTime(), - // ... -}) - -export default Post -``` - -### json - -The `json` method defines a property whose value is a stringified JSON object. - -For example: - -```ts highlights={jsonHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - metadata: model.json(), - // ... -}) - -export default Post -``` - -### array - -The `array` method defines an array of strings property. - -For example: - -```ts highlights={arrHightlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - names: model.array(), - // ... -}) - -export default Post -``` - -### Properties Reference - -Refer to the [Data Model Language (DML) reference](https://docs.medusajs.com/resources/references/data-model/index.html.md) for a full reference of the properties. - -*** - -## Set Primary Key Property - -To set any `id`, `text`, or `number` property as a primary key, use the `primaryKey` method. - -For example: - -```ts highlights={highlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - // ... -}) - -export default Post -``` - -In the example above, the `id` property is defined as the data model's primary key. - -*** - -## Property Default Value - -Use the `default` method on a property's definition to specify the default value of a property. - -For example: - -```ts highlights={defaultHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - color: model - .enum(["black", "white"]) - .default("black"), - age: model - .number() - .default(0), - // ... -}) - -export default Post -``` - -In this example, you set the default value of the `color` enum property to `black`, and that of the `age` number property to `0`. - -*** - -## Make Property Optional - -Use the `nullable` method to indicate that a property’s value can be `null`. This is useful when you want a property to be optional. - -For example: - -```ts highlights={nullableHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - price: model.bigNumber().nullable(), - // ... -}) - -export default Post -``` - -In the example above, the `price` property is configured to allow `null` values, making it optional. - -*** - -## Unique Property - -The `unique` method indicates that a property’s value must be unique in the database through a unique index. - -For example: - -```ts highlights={uniqueHighlights} -import { model } from "@medusajs/framework/utils" - -const User = model.define("user", { - email: model.text().unique(), - // ... -}) - -export default User -``` - -In this example, multiple users can’t have the same email. - -*** - -## Define Database Index on Property - -Use the `index` method on a property's definition to define a database index. - -For example: - -```ts highlights={dbIndexHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - id: model.id().primaryKey(), - name: model.text().index( - "IDX_MY_CUSTOM_NAME" - ), -}) - -export default Post -``` - -The `index` method optionally accepts the name of the index as a parameter. - -In this example, you define an index on the `name` property. - -*** - -## Define a Searchable Property - -Methods generated by the [service factory](https://docs.medusajs.com/learn/fundamentals/modules/service-factory/index.html.md) that accept filters, such as `list{ModelName}s`, accept a `q` property as part of the filters. - -When the `q` filter is passed, the data model's searchable properties are queried to find matching records. - -Use the `searchable` method on a `text` property to indicate that it's searchable. - -For example: - -```ts highlights={searchableHighlights} -import { model } from "@medusajs/framework/utils" - -const Post = model.define("post", { - title: model.text().searchable(), - // ... -}) - -export default Post -``` - -In this example, the `title` property is searchable. - -### Search Example - -If you pass a `q` filter to the `listPosts` method: - -```ts -const posts = await blogModuleService.listPosts({ - q: "New Products", -}) -``` - -This retrieves records that include `New Products` in their `title` property. - - # 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. @@ -12362,293 +10162,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). - - -# Module Link Direction - -In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. - -The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction. - -## Link Direction - -The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. - -For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model: - -```ts -export default defineLink( - BlogModule.linkable.post, - ProductModule.linkable.product -) -``` - -Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model: - -```ts -export default defineLink( - ProductModule.linkable.product, - BlogModule.linkable.post -) -``` - -The above links are two different links that serve different purposes. - -*** - -## Which Link Direction to Use? - -### Extend Data Models - -If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. - -For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: - -```ts -export default defineLink( - ProductModule.linkable.product, - BlogModule.linkable.subtitle -) -``` - -### Associate Data Models - -If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. - -For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: - -```ts -export default defineLink( - BlogModule.linkable.post, - ProductModule.linkable.product -) -``` - - # Read-Only Module Link In this chapter, you’ll learn what a read-only module link is and how to define one. @@ -13155,34 +10668,296 @@ 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, 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). + + +# Module Container + +In this chapter, you'll learn about the module's container and how to resolve resources in that container. + +Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container only used by the resources of that module. + +So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container. + +### List of Registered Resources + +Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). + +*** + +## Resolve Resources + +### Services + +A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. For example: -```ts highlights={highlights} -export default async function myCustomJob() { - console.log("I'll be executed three times only.") +```ts highlights={[["4"], ["10"]]} +import { Logger } from "@medusajs/framework/types" + +type InjectedDependencies = { + logger: Logger } -export const config = { - name: "hello-world", - // execute every minute - schedule: "* * * * *", - numberOfExecutions: 3, +export default class BlogModuleService { + protected logger_: Logger + + constructor({ logger }: InjectedDependencies) { + this.logger_ = logger + + this.logger_.info("[BlogModuleService]: Hello World!") + } + + // ... } ``` -The above scheduled job has the `numberOfExecutions` configuration set to `3`. +### Loader -So, it'll only execute 3 times, each every minute, then it won't be executed anymore. +A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. -If you restart the Medusa application, the scheduled job will be executed again until reaching the number of executions specified. +For example: + +```ts highlights={[["9"]]} +import { + LoaderOptions, +} from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, +} from "@medusajs/framework/utils" + +export default async function helloWorldLoader({ + container, +}: LoaderOptions) { + const logger = container.resolve(ContainerRegistrationKeys.LOGGER) + + logger.info("[helloWorldLoader]: Hello, World!") +} +``` # Create a Plugin @@ -13663,29 +11438,222 @@ export const countProductsStep = createStep( Your workflow can use services of both custom and Commerce Modules, supporting you in building custom flows without having to re-build core commerce features. -# Module Container +# Module Link Direction -In this chapter, you'll learn about the module's container and how to resolve resources in that container. +In this chapter, you'll learn about the difference in module link directions, and which to use based on your use case. -Since modules are [isolated](https://docs.medusajs.com/learn/fundamentals/modules/isolation/index.html.md), each module has a local container only used by the resources of that module. +The details in this chapter don't apply to [Read-Only Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md). Refer to the [Read-Only Module Links chapter](https://docs.medusajs.com/learn/fundamentals/module-links/read-only/index.html.md) for more information on read-only links and their direction. -So, resources in the module, such as services or loaders, can only resolve other resources registered in the module's container, and some Framework tools that the Medusa application registers in the module's container. +## Link Direction -### List of Registered Resources +The module link's direction depends on the order you pass the data model configuration parameters to `defineLink`. -Find a list of resources or dependencies registered in a module's container in [the Container Resources reference](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md). +For example, the following defines a link from the Blog Module's `post` data model to the Product Module's `product` data model: + +```ts +export default defineLink( + BlogModule.linkable.post, + ProductModule.linkable.product +) +``` + +Whereas the following defines a link from the Product Module's `product` data model to the Blog Module's `post` data model: + +```ts +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.post +) +``` + +The above links are two different links that serve different purposes. *** -## Resolve Resources +## Which Link Direction to Use? -### Services +### Extend Data Models -A service's constructor accepts as a first parameter an object used to resolve resources registered in the module's container. +If you're adding a link to a data model to extend it and add new fields, define the link from the main data model to the custom data model. + +For example, consider you want to add a `subtitle` custom field to the `product` data model. To do that, you define a `Subtitle` data model in your module, then define a link from the `Product` data model to it: + +```ts +export default defineLink( + ProductModule.linkable.product, + BlogModule.linkable.subtitle +) +``` + +### Associate Data Models + +If you're linking data models to indicate an association between them, define the link from the custom data model to the main data model. + +For example, consider you have `Post` data model representing a blog post, and you want to associate a blog post with a product. To do that, define a link from the `Post` data model to `Product`: + +```ts +export default defineLink( + BlogModule.linkable.post, + ProductModule.linkable.product +) +``` + + +# Infrastructure Modules + +In this chapter, you’ll learn about Infrastructure Modules. + +## What is an Infrastructure Module? + +An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. + +Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. + +*** + +## Infrastructure Module Types + +There are different Infrastructure Module types including: + +![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) + +- Analytics Module: Integrates a third-party service to track and analyze user interactions and system events. +- Cache Module: Defines the caching mechanism or logic to cache computational results. +- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. +- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. +- File Module: Integrates a storage service to handle uploading and managing files. +- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. +- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. + +*** + +## Infrastructure Modules List + +Refer to the [Infrastructure Modules reference](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) for a list of Medusa’s Infrastructure Modules, available modules to install, and how to create an Infrastructure Module. + + +# Module Isolation + +In this chapter, you'll learn how modules are isolated, and what that means for your custom development. + +- Modules can't access resources, such as services or data models, from other modules. +- Use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to extend an existing module's data models, and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. +- Use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build features that depend on functionalities from different modules. + +## How are Modules Isolated? + +A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. + +For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. + +A module has its own container, as explained in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides. + +Refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container. + +*** + +## Why are Modules Isolated + +Some of the module isolation's benefits include: + +- Integrate your module into any Medusa application without side-effects to your setup. +- Replace existing modules with your custom implementation if your use case is drastically different. +- Use modules in other environments, such as Edge functions and Next.js apps. + +*** + +## How to Extend Data Model of Another Module? + +To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). Module Links allow you to build associations between data models of different modules without breaking the module isolation. + +Then, you can retrieve data across modules using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +*** + +## How to Use Services of Other Modules? + +You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service. + +To build functionalities spanning across modules and systems, create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) whose steps resolve the modules' services to perform these functionalities. + +Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. + +### Example + +For example, consider you have two modules: + +1. A module that stores and manages brands in your application. +2. A module that integrates a third-party Content Management System (CMS). + +To sync brands from your application to the third-party system, create the following steps: + +```ts title="Example Steps" highlights={stepsHighlights} +const retrieveBrandsStep = createStep( + "retrieve-brands", + async (_, { container }) => { + const brandModuleService = container.resolve( + "brand" + ) + + const brands = await brandModuleService.listBrands() + + return new StepResponse(brands) + } +) + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cms" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cms" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) +``` + +The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module. + +Then, create the following workflow that uses these steps: + +```ts title="Example Workflow" +export const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const brands = retrieveBrandsStep() + + createBrandsInCmsStep({ brands }) + } +) +``` + +You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. + +*** + +## How to Use Framework APIs and Tools in Module? + +### Framework Tools in Module Container + +A module has in its container some Framework APIs and tools, such as [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md). You can refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container. + +You can resolve those resources in the module's services and loaders. For example: -```ts highlights={[["4"], ["10"]]} +```ts title="Example Service" import { Logger } from "@medusajs/framework/types" type InjectedDependencies = { @@ -13705,26 +11673,114 @@ export default class BlogModuleService { } ``` -### Loader +In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message. -A loader function accepts as a parameter an object having the property `container`. Its value is the module's container used to resolve resources. +### Using Framework Tools in Workflows + +Some Framework APIs and tools are not registered in the module's container. For example, [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) is only registered in the Medusa container. + +You should, instead, build workflows that use these APIs and tools along with your module's service. + +For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action. + +```ts title="Example Workflow" +import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +const createBrandsInCmsStep = createStep( + "create-brands-in-cms", + async ({ brands }, { container }) => { + const cmsModuleService = container.resolve( + "cms" + ) + + const cmsBrands = await cmsModuleService.createBrands(brands) + + return new StepResponse(cmsBrands, cmsBrands) + }, + async (brands, { container }) => { + const cmsModuleService = container.resolve( + "cms" + ) + + await cmsModuleService.deleteBrands( + brands.map((brand) => brand.id) + ) + } +) + +const syncBrandsWorkflow = createWorkflow( + "sync-brands", + () => { + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: [ + "*", + "products.*", + ], + }) + + createBrandsInCmsStep({ brands }) + } +) +``` + +In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step. + +In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service. + +### Injecting Dependencies to Module + +Some cases still require you to access external resources, mainly [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) or Framework tools, in your module. +For example, you may need the [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md) to emit events from your module's service. + +In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration. + +Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes. For example: -```ts highlights={[["9"]]} -import { - LoaderOptions, -} from "@medusajs/framework/types" -import { - ContainerRegistrationKeys, -} from "@medusajs/framework/utils" +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -export default async function helloWorldLoader({ - container, -}: LoaderOptions) { - const logger = container.resolve(ContainerRegistrationKeys.LOGGER) +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "./src/modules/blog", + dependencies: [ + Modules.EVENT_BUS, + ], + }, + ], +}) +``` - logger.info("[helloWorldLoader]: Hello, World!") +In this example, you inject the Event Module's service to your module's container. + +Only the main service will be injected into the module's container. + +You can then use the Event Module's service in your module's service: + +```ts title="Example Service" +class BlogModuleService { + protected eventBusService_: AbstractEventBusModuleService + + constructor({ event_bus }) { + this.eventBusService_ = event_bus + } + + performAction() { + // TODO perform action + + this.eventBusService_.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } } ``` @@ -14336,37 +12392,31 @@ class BlogModuleService { ``` -# Infrastructure Modules +# Modules Directory Structure -In this chapter, you’ll learn about Infrastructure Modules. +In this document, you'll learn about the expected files and directories in your module. -## What is an Infrastructure Module? +![Module Directory Structure Example](https://res.cloudinary.com/dza7lstvk/image/upload/v1714379976/Medusa%20Book/modules-dir-overview_nqq7ne.jpg) -An Infrastructure Module implements features and mechanisms related to the Medusa application’s architecture and infrastructure. +## index.ts -Since modules are interchangeable, you have more control over Medusa’s architecture. For example, you can choose to use Memcached for event handling instead of Redis. +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). *** -## Infrastructure Module Types +## service.ts -There are different Infrastructure Module types including: - -![Diagram illustrating how the modules connect to third-party services](https://res.cloudinary.com/dza7lstvk/image/upload/v1727095814/Medusa%20Book/architectural-modules_bj9bb9.jpg) - -- Analytics Module: Integrates a third-party service to track and analyze user interactions and system events. -- Cache Module: Defines the caching mechanism or logic to cache computational results. -- Event Module: Integrates a pub/sub service to handle subscribing to and emitting events. -- Workflow Engine Module: Integrates a service to store and track workflow executions and steps. -- File Module: Integrates a storage service to handle uploading and managing files. -- Notification Module: Integrates a third-party service or defines custom logic to send notifications to users and customers. -- Locking Module: Integrates a service that manages access to shared resources by multiple processes or threads. +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). *** -## Infrastructure Modules List +## Other Directories -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. +The following directories are optional and their content are explained more in the following chapters: + +- `models`: Holds the data models representing tables in the database. +- `migrations`: Holds the migration files used to reflect changes on the database. +- `loaders`: Holds the scripts to run on the Medusa application's start-up. # Loaders @@ -14628,286 +12678,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. - - -# Module Isolation - -In this chapter, you'll learn how modules are isolated, and what that means for your custom development. - -- Modules can't access resources, such as services or data models, from other modules. -- Use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md) to extend an existing module's data models, and [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. -- Use [workflows](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) to build features that depend on functionalities from different modules. - -## How are Modules Isolated? - -A module is unaware of any resources other than its own, such as services or data models. This means it can't access these resources if they're implemented in another module. - -For example, your custom module can't resolve the Product Module's main service or have direct relationships from its data model to the Product Module's data models. - -A module has its own container, as explained in the [Module Container](https://docs.medusajs.com/learn/fundamentals/modules/container/index.html.md) chapter. This container includes the module's resources, such as services and data models, and some Framework resources that the Medusa application provides. - -Refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container. - -*** - -## Why are Modules Isolated - -Some of the module isolation's benefits include: - -- Integrate your module into any Medusa application without side-effects to your setup. -- Replace existing modules with your custom implementation if your use case is drastically different. -- Use modules in other environments, such as Edge functions and Next.js apps. - -*** - -## How to Extend Data Model of Another Module? - -To extend the data model of another module, such as the `Product` data model of the Product Module, use [Module Links](https://docs.medusajs.com/learn/fundamentals/module-links/index.html.md). Module Links allow you to build associations between data models of different modules without breaking the module isolation. - -Then, you can retrieve data across modules using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). - -*** - -## How to Use Services of Other Modules? - -You'll often build feature that uses functionalities from different modules. For example, if you may need to retrieve brands, then sync them to a third-party service. - -To build functionalities spanning across modules and systems, create a [workflow](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md) whose steps resolve the modules' services to perform these functionalities. - -Workflows ensure data consistency through their roll-back mechanism and tracking of each execution's status, steps, input, and output. - -### Example - -For example, consider you have two modules: - -1. A module that stores and manages brands in your application. -2. A module that integrates a third-party Content Management System (CMS). - -To sync brands from your application to the third-party system, create the following steps: - -```ts title="Example Steps" highlights={stepsHighlights} -const retrieveBrandsStep = createStep( - "retrieve-brands", - async (_, { container }) => { - const brandModuleService = container.resolve( - "brand" - ) - - const brands = await brandModuleService.listBrands() - - return new StepResponse(brands) - } -) - -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cms" - ) - - const cmsBrands = await cmsModuleService.createBrands(brands) - - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cms" - ) - - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) -``` - -The `retrieveBrandsStep` retrieves the brands from a Brand Module, and the `createBrandsInCmsStep` creates the brands in a third-party system using a CMS Module. - -Then, create the following workflow that uses these steps: - -```ts title="Example Workflow" -export const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const brands = retrieveBrandsStep() - - createBrandsInCmsStep({ brands }) - } -) -``` - -You can then use this workflow in an API route, scheduled job, or other resources that use this functionality. - -*** - -## How to Use Framework APIs and Tools in Module? - -### Framework Tools in Module Container - -A module has in its container some Framework APIs and tools, such as [Logger](https://docs.medusajs.com/learn/debugging-and-testing/logging/index.html.md). You can refer to the [Module Container Resources](https://docs.medusajs.com/resources/medusa-container-resources/index.html.md) for a list of resources registered in a module's container. - -You can resolve those resources in the module's services and loaders. - -For example: - -```ts title="Example Service" -import { Logger } from "@medusajs/framework/types" - -type InjectedDependencies = { - logger: Logger -} - -export default class BlogModuleService { - protected logger_: Logger - - constructor({ logger }: InjectedDependencies) { - this.logger_ = logger - - this.logger_.info("[BlogModuleService]: Hello World!") - } - - // ... -} -``` - -In this example, the `BlogModuleService` class resolves the `Logger` service from the module's container and uses it to log a message. - -### Using Framework Tools in Workflows - -Some Framework APIs and tools are not registered in the module's container. For example, [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md) is only registered in the Medusa container. - -You should, instead, build workflows that use these APIs and tools along with your module's service. - -For example, you can create a workflow that retrieves data using Query, then pass the data to your module's service to perform some action. - -```ts title="Example Workflow" -import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -const createBrandsInCmsStep = createStep( - "create-brands-in-cms", - async ({ brands }, { container }) => { - const cmsModuleService = container.resolve( - "cms" - ) - - const cmsBrands = await cmsModuleService.createBrands(brands) - - return new StepResponse(cmsBrands, cmsBrands) - }, - async (brands, { container }) => { - const cmsModuleService = container.resolve( - "cms" - ) - - await cmsModuleService.deleteBrands( - brands.map((brand) => brand.id) - ) - } -) - -const syncBrandsWorkflow = createWorkflow( - "sync-brands", - () => { - const { data: brands } = useQueryGraphStep({ - entity: "brand", - fields: [ - "*", - "products.*", - ], - }) - - createBrandsInCmsStep({ brands }) - } -) -``` - -In this example, you use the `useQueryGraphStep` to retrieve brands with their products, then pass the brands to the `createBrandsInCmsStep` step. - -In the `createBrandsInCmsStep`, you resolve the CMS Module's service from the module's container and use it to create the brands in the third-party system. You pass the brands you retrieved using Query to the module's service. - -### Injecting Dependencies to Module - -Some cases still require you to access external resources, mainly [Infrastructure Modules](https://docs.medusajs.com/resources/infrastructure-modules/index.html.md) or Framework tools, in your module. -For example, you may need the [Event Module](https://docs.medusajs.com/resources/infrastructure-modules/event/index.html.md) to emit events from your module's service. - -In those cases, you can inject the dependencies to your module's service in `medusa-config.ts` using the `dependencies` property of the module's configuration. - -Use this approach only when absolutely necessary, where workflows aren't sufficient for your use case. By injecting dependencies, you risk breaking your module if the dependency isn't provided, or if the dependency's API changes. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/blog", - dependencies: [ - Modules.EVENT_BUS, - ], - }, - ], -}) -``` - -In this example, you inject the Event Module's service to your module's container. - -Only the main service will be injected into the module's container. - -You can then use the Event Module's service in your module's service: - -```ts title="Example Service" -class BlogModuleService { - protected eventBusService_: AbstractEventBusModuleService - - constructor({ event_bus }) { - this.eventBusService_ = event_bus - } - - performAction() { - // TODO perform action - - this.eventBusService_.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, - }) - } -} -``` - - # Multiple Services in a Module In this chapter, you'll learn how to use multiple services in a module. @@ -15376,6 +13146,504 @@ export default Module(BLOG_MODULE, { Now, when the Medusa application starts, the loader will run, validating the module's options and throwing an error if the `apiKey` option is missing. +# 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. + + +# Service Constraints + +This chapter lists constraints to keep in mind when creating a service. + +## Use Async Methods + +Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. + +For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: + +```ts +await blogModuleService.getMessage() +``` + +So, make sure your service's methods are always async to avoid unexpected errors or behavior. + +```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} +import { MedusaService } from "@medusajs/framework/utils" +import Post from "./models/post" + +class BlogModuleService extends MedusaService({ + Post, +}){ + // Don't + getMessage(): string { + return "Hello, World!" + } + + // Do + async getMessage(): Promise { + return "Hello, World!" + } +} + +export default BlogModuleService +``` + + +# 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: Query Product's Brands + +In the previous chapters, you [defined a link](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) between the [custom Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) and Medusa's [Product Module](https://docs.medusajs.com/resources/commerce-modules/product/index.html.md), then [extended the create-product flow](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product/index.html.md) to link a product to a brand. + +In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. + +### Prerequisites + +- [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) +- [Defined link between the Brand and Product data models.](https://docs.medusajs.com/learn/customization/extend-features/define-link/index.html.md) + +*** + +## Approach 1: Retrieve Brands in Existing API Routes + +Medusa's existing API routes accept a `fields` query parameter that allows you to specify the fields and relations of a model to retrieve. So, when you send a request to the [List Products](https://docs.medusajs.com/api/admin#products_getproducts), [Get Product](https://docs.medusajs.com/api/admin#products_getproductsid), or any product-related store or admin routes that accept a `fields` query parameter, you can specify in this parameter to return the product's brands. + +Learn more about using the `fields` query parameter to retrieve custom linked data models in the [Retrieve Custom Linked Data Models from Medusa's API Routes](https://docs.medusajs.com/learn/fundamentals/api-routes/retrieve-custom-links/index.html.md) chapter. + +For example, send the following request to retrieve the list of products with their brands: + +```bash +curl 'http://localhost:9000/admin/products?fields=+brand.*' \ +--header 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +Any product that is linked to a brand will have a `brand` property in its object: + +```json title="Example Product Object" +{ + "id": "prod_123", + // ... + "brand": { + "id": "01JEB44M61BRM3ARM2RRMK7GJF", + "name": "Acme", + "created_at": "2024-12-05T09:59:08.737Z", + "updated_at": "2024-12-05T09:59:08.737Z", + "deleted_at": null + } +} +``` + +By using the `fields` query parameter, you don't have to re-create existing API routes to get custom data models that you linked to core data models. + +### Limitations: Filtering by Brands in Existing API Routes + +While you can retrieve linked records using the `fields` query parameter of an existing API route, you can't filter by linked records. + +Instead, you'll have to create a custom API route that uses Query to retrieve linked records with filters, as explained in the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Approach 2: Use Query to Retrieve Linked Records + +You can also retrieve linked records using Query. Query allows you to retrieve data across modules with filters, pagination, and more. You can resolve Query from the Medusa container and use it in your API route or workflow. + +Learn more about Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). + +For example, you can create an API route that retrieves brands and their products. If you followed the [Create Brands API route chapter](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md), you'll have the file `src/api/admin/brands/route.ts` with a `POST` API route. Add a new `GET` function to the same file: + +```ts title="src/api/admin/brands/route.ts" highlights={highlights} +// other imports... +import { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve("query") + + const { data: brands } = await query.graph({ + entity: "brand", + fields: ["*", "products.*"], + }) + + res.json({ brands }) +} +``` + +This adds a `GET` API route at `/admin/brands`. In the API route, you resolve Query from the Medusa container. Query has a `graph` method that runs a query to retrieve data. It accepts an object having the following properties: + +- `entity`: The data model's name as specified in the first parameter of `model.define`. +- `fields`: An array of properties and relations to retrieve. You can pass: + - A property's name, such as `id`, or `*` for all properties. + - A relation or linked model's name, such as `products` (use the plural name since brands are linked to list of products). You suffix the name with `.*` to retrieve all its properties. + +`graph` returns an object having a `data` property, which is the retrieved brands. You return the brands in the response. + +### Test it Out + +To test the API route out, send a `GET` request to `/admin/brands`: + +```bash +curl 'http://localhost:9000/admin/brands' \ +-H 'Authorization: Bearer {token}' +``` + +Make sure to replace `{token}` with your admin user's authentication token. Learn how to retrieve it in the [API reference](https://docs.medusajs.com/api/store#authentication). + +This returns the brands in your store with their linked products. For example: + +```json title="Example Response" +{ + "brands": [ + { + "id": "123", + // ... + "products": [ + { + "id": "prod_123", + // ... + } + ] + } + ] +} +``` + +### Limitations: Filtering by Brand in Query + +While you can use Query to retrieve linked records, you can't filter by linked records. + +For an alternative approach, refer to the [Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-filters-and-pagination-on-linked-records/index.html.md). + +*** + +## Summary + +By following the examples of the previous chapters, you: + +- Defined a link between the Brand and Product modules's data models, allowing you to associate a product with a brand. +- Extended the create-product workflow and route to allow setting the product's brand while creating the product. +- Queried a product's brand, and vice versa. + +*** + +## Next Steps: Customize Medusa Admin + +Clients, such as the Medusa Admin dashboard, can now use brand-related features, such as creating a brand or setting the brand of a product. + +In the next chapters, you'll learn how to customize the Medusa Admin to show a product's brand on its details page, and to show a new page with the list of brands in your store. + + # Expose a Workflow Hook In this chapter, you'll learn how to expose a hook in your workflow. @@ -15445,203 +13713,6 @@ The hook is available on the workflow's `hooks` property using its name `product You invoke the hook, passing a step function (the hook handler) as a parameter. -# Service Constraints - -This chapter lists constraints to keep in mind when creating a service. - -## Use Async Methods - -Medusa wraps service method executions to inject useful context or transactions. However, since Medusa can't detect whether the method is asynchronous, it always executes methods in the wrapper with the `await` keyword. - -For example, if you have a synchronous `getMessage` method, and you use it in other resources like workflows, Medusa executes it as an async method: - -```ts -await blogModuleService.getMessage() -``` - -So, make sure your service's methods are always async to avoid unexpected errors or behavior. - -```ts highlights={[["8", "", "Method must be async."], ["13", "async", "Correct way of defining the method."]]} -import { MedusaService } from "@medusajs/framework/utils" -import Post from "./models/post" - -class BlogModuleService extends MedusaService({ - Post, -}){ - // Don't - getMessage(): string { - return "Hello, World!" - } - - // Do - async getMessage(): Promise { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - - -# Conditions in Workflows with When-Then - -In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. - -## Why If-Conditions Aren't Allowed in Workflows? - -Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. - -So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. - -Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. - -Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. - -*** - -## How to use When-Then? - -The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. - -For example: - -```ts highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - when, -} from "@medusajs/framework/workflows-sdk" -// step imports... - -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const result = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - const stepResult = isActiveStep() - return stepResult - }) - - // executed without condition - const anotherStepResult = anotherStep(result) - - return new WorkflowResponse( - anotherStepResult - ) - } -) -``` - -In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. - -### When Parameters - -`when` accepts the following parameters: - -1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. - -### Then Parameters - -To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. - -The callback function is only executed if `when`'s second parameter function returns a `true` value. - -*** - -## Implementing If-Else with When-Then - -when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. - -For example: - -```ts highlights={ifElseHighlights} -const workflow = createWorkflow( - "workflow", - function (input: { - is_active: boolean - }) { - - const isActiveResult = when( - input, - (input) => { - return input.is_active - } - ).then(() => { - return isActiveStep() - }) - - const notIsActiveResult = when( - input, - (input) => { - return !input.is_active - } - ).then(() => { - return notIsActiveStep() - }) - - // ... - } -) -``` - -In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. - -*** - -## Specify Name for When-Then - -Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: - -```ts -const isActiveResult = when( - input, - (input) => { - return input.is_active - } -).then(() => { - return isActiveStep() -}) -``` - -This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. - -However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. - -You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: - -```ts highlights={nameHighlights} -const { isActive } = when( - "check-is-active", - input, - (input) => { - return input.is_active - } -).then(() => { - const isActive = isActiveStep() - - return { - isActive, - } -}) -``` - -Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: - -1. A unique name to be assigned to the `when-then` block. -2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. -3. A function that returns a boolean indicating whether to execute the action in `then`. - -The second and third parameters are the same as the parameters you previously passed to `when`. - - # Compensation Function In this chapter, you'll learn what a compensation function is and how to add it to a step. @@ -16252,6 +14323,295 @@ const step1 = createStep( ``` +# Conditions in Workflows with When-Then + +In this chapter, you'll learn how to execute an action based on a condition in a workflow using when-then from the Workflows SDK. + +## Why If-Conditions Aren't Allowed in Workflows? + +Medusa creates an internal representation of the workflow definition you pass to `createWorkflow` to track and store its steps. At that point, variables in the workflow don't have any values. They only do when you execute the workflow. + +So, you can't use an if-condition that checks a variable's value, as the condition will be evaluated when Medusa creates the internal representation of the workflow, rather than during execution. + +Instead, use when-then from the Workflows SDK. It allows you to perform steps in a workflow only if a condition that you specify is satisfied. + +Restrictions for conditions is only applicable in a workflow's definition. You can still use if-conditions in your step's code. + +*** + +## How to use When-Then? + +The Workflows SDK provides a `when` function that is used to check whether a condition is true. You chain a `then` function to `when` that specifies the steps to execute if the condition in `when` is satisfied. + +For example: + +```ts highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + when, +} from "@medusajs/framework/workflows-sdk" +// step imports... + +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const result = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + const stepResult = isActiveStep() + return stepResult + }) + + // executed without condition + const anotherStepResult = anotherStep(result) + + return new WorkflowResponse( + anotherStepResult + ) + } +) +``` + +In this code snippet, you execute the `isActiveStep` only if the `input.is_active`'s value is `true`. + +### When Parameters + +`when` accepts the following parameters: + +1. The first parameter is either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +2. The second parameter is a function that returns a boolean indicating whether to execute the action in `then`. + +### Then Parameters + +To specify the action to perform if the condition is satisfied, chain a `then` function to `when` and pass it a callback function. + +The callback function is only executed if `when`'s second parameter function returns a `true` value. + +*** + +## Implementing If-Else with When-Then + +when-then doesn't support if-else conditions. Instead, use two `when-then` conditions in your workflow. + +For example: + +```ts highlights={ifElseHighlights} +const workflow = createWorkflow( + "workflow", + function (input: { + is_active: boolean + }) { + + const isActiveResult = when( + input, + (input) => { + return input.is_active + } + ).then(() => { + return isActiveStep() + }) + + const notIsActiveResult = when( + input, + (input) => { + return !input.is_active + } + ).then(() => { + return notIsActiveStep() + }) + + // ... + } +) +``` + +In the above workflow, you use two `when-then` blocks. The first one performs a step if `input.is_active` is `true`, and the second performs a step if `input.is_active` is `false`, acting as an else condition. + +*** + +## Specify Name for When-Then + +Internally, `when-then` blocks have a unique name similar to a step. When you return a step's result in a `when-then` block, the block's name is derived from the step's name. For example: + +```ts +const isActiveResult = when( + input, + (input) => { + return input.is_active + } +).then(() => { + return isActiveStep() +}) +``` + +This `when-then` block's internal name will be `when-then-is-active`, where `is-active` is the step's name. + +However, if you need to return in your `when-then` block something other than a step's result, you need to specify a unique step name for that block. Otherwise, Medusa will generate a random name for it which can cause unexpected errors in production. + +You pass a name for `when-then` as a first parameter of `when`, whose signature can accept three parameters in this case. For example: + +```ts highlights={nameHighlights} +const { isActive } = when( + "check-is-active", + input, + (input) => { + return input.is_active + } +).then(() => { + const isActive = isActiveStep() + + return { + isActive, + } +}) +``` + +Since `then` returns a value different than the step's result, you pass to the `when` function the following parameters: + +1. A unique name to be assigned to the `when-then` block. +2. Either an object or the workflow's input. This data is passed as a parameter to the function in `when`'s second parameter. +3. A function that returns a boolean indicating whether to execute the action in `then`. + +The second and third parameters are the same as the parameters you previously passed to `when`. + + +# Execute Another Workflow + +In this chapter, you'll learn how to execute a workflow in another. + +## Execute in a Workflow + +To execute a workflow in another, use the `runAsStep` method that every workflow has. + +For example: + +```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" +import { + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +const workflow = createWorkflow( + "hello-world", + async (input) => { + const products = createProductsWorkflow.runAsStep({ + input: { + products: [ + // ... + ], + }, + }) + + // ... + } +) +``` + +Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. + +The object has an `input` property to pass input to the workflow. + +*** + +## Preparing Input Data + +If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. + +Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +For example: + +```ts highlights={transformHighlights} collapsibleLines="1-12" +import { + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" + +type WorkflowInput = { + title: string +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const createProductsData = transform({ + input, + }, (data) => [ + { + title: `Hello ${data.input.title}`, + }, + ]) + + const products = createProductsWorkflow.runAsStep({ + input: { + products: createProductsData, + }, + }) + + // ... + } +) +``` + +In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. + +*** + +## Run Workflow Conditionally + +To run a workflow in another based on a condition, use when-then from the Workflows SDK. + +Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). + +For example: + +```ts highlights={whenHighlights} collapsibleLines="1-16" +import { + createWorkflow, + when, +} from "@medusajs/framework/workflows-sdk" +import { + createProductsWorkflow, +} from "@medusajs/medusa/core-flows" +import { + CreateProductWorkflowInputDTO, +} from "@medusajs/framework/types" + +type WorkflowInput = { + product?: CreateProductWorkflowInputDTO + should_create?: boolean +} + +const workflow = createWorkflow( + "hello-product", + async (input: WorkflowInput) => { + const product = when(input, ({ should_create }) => should_create) + .then(() => { + return createProductsWorkflow.runAsStep({ + input: { + products: [input.product], + }, + }) + }) + } +) +``` + +In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. + + # 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. @@ -16792,263 +15152,6 @@ To find a full example of a long-running workflow, refer to the [restaurant-deli In the recipe, you use a long-running workflow that moves an order from placed to completed. The workflow waits for the restaurant to accept the order, the driver to pick up the order, and other external actions. -# Execute Another Workflow - -In this chapter, you'll learn how to execute a workflow in another. - -## Execute in a Workflow - -To execute a workflow in another, use the `runAsStep` method that every workflow has. - -For example: - -```ts highlights={workflowsHighlights} collapsibleLines="1-7" expandMoreButton="Show Imports" -import { - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -const workflow = createWorkflow( - "hello-world", - async (input) => { - const products = createProductsWorkflow.runAsStep({ - input: { - products: [ - // ... - ], - }, - }) - - // ... - } -) -``` - -Instead of invoking the workflow and passing it the container, you use its `runAsStep` method and pass it an object as a parameter. - -The object has an `input` property to pass input to the workflow. - -*** - -## Preparing Input Data - -If you need to perform some data manipulation to prepare the other workflow's input data, use `transform` from the Workflows SDK. - -Learn about transform in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -For example: - -```ts highlights={transformHighlights} collapsibleLines="1-12" -import { - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" - -type WorkflowInput = { - title: string -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const createProductsData = transform({ - input, - }, (data) => [ - { - title: `Hello ${data.input.title}`, - }, - ]) - - const products = createProductsWorkflow.runAsStep({ - input: { - products: createProductsData, - }, - }) - - // ... - } -) -``` - -In this example, you use the `transform` function to prepend `Hello` to the title of the product. Then, you pass the result as an input to the `createProductsWorkflow`. - -*** - -## Run Workflow Conditionally - -To run a workflow in another based on a condition, use when-then from the Workflows SDK. - -Learn about when-then in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/conditions/index.html.md). - -For example: - -```ts highlights={whenHighlights} collapsibleLines="1-16" -import { - createWorkflow, - when, -} from "@medusajs/framework/workflows-sdk" -import { - createProductsWorkflow, -} from "@medusajs/medusa/core-flows" -import { - CreateProductWorkflowInputDTO, -} from "@medusajs/framework/types" - -type WorkflowInput = { - product?: CreateProductWorkflowInputDTO - should_create?: boolean -} - -const workflow = createWorkflow( - "hello-product", - async (input: WorkflowInput) => { - const product = when(input, ({ should_create }) => should_create) - .then(() => { - return createProductsWorkflow.runAsStep({ - input: { - products: [input.product], - }, - }) - }) - } -) -``` - -In this example, you use when-then to run the `createProductsWorkflow` only if `should_create` (passed in the `input`) is enabled. - - -# Run Workflow Steps in Parallel - -In this chapter, you’ll learn how to run workflow steps in parallel. - -## 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`. - - # Retry Failed Steps In this chapter, you’ll learn how to configure steps to allow retrial on failure. @@ -17317,128 +15420,131 @@ if (workflowExecution.state === "failed") { Other state values include `done`, `invoking`, and `compensating`. -# Workflow Hooks +# Run Workflow Steps in Parallel -In this chapter, you'll learn what a workflow hook is and how to consume them. +In this chapter, you’ll learn how to run workflow steps in parallel. -## What is a Workflow Hook? +## parallelize Utility Function -A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. +If your workflow has steps that don’t rely on one another’s results, run them in parallel using `parallelize` from the Workflows SDK. -Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. - -Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. - -You want to perform a custom action during a workflow's execution, such as when a product is created. - -*** - -## How to Consume a Hook? - -A workflow has a special `hooks` property which is an object that holds its hooks. - -So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: - -- Import the workflow. -- Access its hook using the `hooks` property. -- Pass the hook a step function as a parameter to consume it. - -For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: - -```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - } -) -``` - -The `productsCreated` hook is available on the workflow's `hooks` property by its name. - -You invoke the hook, passing a step function (the hook handler) as a parameter. - -Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. - -A hook can have only one handler. - -Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. - -### Hook Handler Parameter - -Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. - -Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. - -### Hook Handler Compensation - -Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. +The workflow waits until all steps passed to the `parallelize` function finish executing before continuing to the next step. For example: -```ts title="src/workflows/hooks/product-created.ts" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" +```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" -createProductsWorkflow.hooks.productsCreated( - async ({ products }, { container }) => { - // TODO perform an action - - return new StepResponse(undefined, { ids }) - }, - async ({ ids }, { container }) => { - // undo the performed action - } -) -``` - -The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. - -The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. - -It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. - -### Additional Data Property - -Medusa's workflows pass in the hook's input an `additional_data` property: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -createProductsWorkflow.hooks.productsCreated( - async ({ products, additional_data }, { container }) => { - // TODO perform an action - } -) -``` - -This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. - -Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). - -### Pass Additional Data to Workflow - -You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: - -```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} -import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { createProductsWorkflow } from "@medusajs/medusa/core-flows" - -export async function POST(req: MedusaRequest, res: MedusaResponse) { - await createProductsWorkflow(req.scope).run({ - input: { - products: [ - // ... - ], - additional_data: { - custom_field: "test", - }, - }, - }) +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) + } +) ``` -Your hook handler then receives that passed data in the `additional_data` object. +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`. # Data Manipulation in Workflows with transform @@ -17646,6 +15752,338 @@ const myWorkflow = createWorkflow( ``` +# Workflow Hooks + +In this chapter, you'll learn what a workflow hook is and how to consume them. + +## What is a Workflow Hook? + +A workflow hook is a point in a workflow where you can inject custom functionality as a step function, called a hook handler. + +Medusa exposes hooks in many of its workflows that are used in its API routes. You can consume those hooks to add your custom logic. + +Refer to the [Workflows Reference](https://docs.medusajs.com/resources/medusa-workflows-reference/index.html.md) to view all workflows and their hooks. + +You want to perform a custom action during a workflow's execution, such as when a product is created. + +*** + +## How to Consume a Hook? + +A workflow has a special `hooks` property which is an object that holds its hooks. + +So, in a TypeScript or JavaScript file created under the `src/workflows/hooks` directory: + +- Import the workflow. +- Access its hook using the `hooks` property. +- Pass the hook a step function as a parameter to consume it. + +For example, to consume the `productsCreated` hook of Medusa's `createProductsWorkflow`, create the file `src/workflows/hooks/product-created.ts` with the following content: + +```ts title="src/workflows/hooks/product-created.ts" highlights={handlerHighlights} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + } +) +``` + +The `productsCreated` hook is available on the workflow's `hooks` property by its name. + +You invoke the hook, passing a step function (the hook handler) as a parameter. + +Now, when a product is created using the [Create Product API route](https://docs.medusajs.com/api/admin#products_postproducts), your hook handler is executed after the product is created. + +A hook can have only one handler. + +Refer to the [createProductsWorkflow reference](https://docs.medusajs.com/resources/references/medusa-workflows/createProductsWorkflow/index.html.md) to see at which point the hook handler is executed. + +### Hook Handler Parameter + +Since a hook handler is essentially a step function, it receives the hook's input as a first parameter, and an object holding a `container` property as a second parameter. + +Each hook has different input. For example, the `productsCreated` hook receives an object having a `products` property holding the created product. + +### Hook Handler Compensation + +Since the hook handler is a step function, you can set its compensation function as a second parameter of the hook. + +For example: + +```ts title="src/workflows/hooks/product-created.ts" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products }, { container }) => { + // TODO perform an action + + return new StepResponse(undefined, { ids }) + }, + async ({ ids }, { container }) => { + // undo the performed action + } +) +``` + +The compensation function is executed if an error occurs in the workflow to undo the actions performed by the hook handler. + +The compensation function receives as an input the second parameter passed to the `StepResponse` returned by the step function. + +It also accepts as a second parameter an object holding a `container` property to resolve resources from the Medusa container. + +### Additional Data Property + +Medusa's workflows pass in the hook's input an `additional_data` property: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["4", "additional_data"]]} +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +createProductsWorkflow.hooks.productsCreated( + async ({ products, additional_data }, { container }) => { + // TODO perform an action + } +) +``` + +This property is an object that holds additional data passed to the workflow through the request sent to the API route using the workflow. + +Learn how to pass `additional_data` in requests to API routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/api-routes/additional-data/index.html.md). + +### Pass Additional Data to Workflow + +You can also pass that additional data when executing the workflow. Pass it as a parameter to the `.run` method of the workflow: + +```ts title="src/workflows/hooks/product-created.ts" highlights={[["10", "additional_data"]]} +import type { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { createProductsWorkflow } from "@medusajs/medusa/core-flows" + +export async function POST(req: MedusaRequest, res: MedusaResponse) { + await createProductsWorkflow(req.scope).run({ + input: { + products: [ + // ... + ], + additional_data: { + custom_field: "test", + }, + }, + }) +} +``` + +Your hook handler then receives that passed data in the `additional_data` object. + + +# 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. + + # Workflow Timeout In this chapter, you’ll learn how to set a timeout for workflows and steps. @@ -17732,6 +16170,1568 @@ 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`. +# 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(), +}) +``` + +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). + +*** + +## 3. Create Module Service + +You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. + +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/brand", + }, + ], +}) +``` + +The Brand Module is now added to your Medusa application. You'll start using it in the [next chapter](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md). + +*** + +## 6. Generate and Run Migrations + +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 +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 Step: Create Brand Workflow + +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. + + +# Create Brands UI Route in Admin + +In this chapter, you'll add a UI route to the admin dashboard that shows all [brands](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md) in a new page. You'll retrieve the brands from the server and display them in a table with pagination. + +### Prerequisites + +- [Brands Module](https://docs.medusajs.com/learn/customization/custom-features/modules/index.html.md) + +## 1. Get Brands API Route + +In a [previous chapter](https://docs.medusajs.com/learn/customization/extend-features/query-linked-records/index.html.md), you learned how to add an API route that retrieves brands and their products using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. + +Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: + +```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} +// 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, + metadata: { count, take, skip } = {}, + } = await query.graph({ + entity: "brand", + ...req.queryConfig, + }) + + res.json({ + brands, + count, + limit: take, + offset: skip, + }) +} +``` + +In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `queryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. + +The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: + +- `fields`: The fields to retrieve in the brands. +- `limit`: The maximum number of items to retrieve. +- `offset`: The number of items to skip before retrieving the returned items. + +When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: + +- `count`: The total count of items. +- `take`: The maximum number of items returned in the `data` array. +- `skip`: The number of items skipped before retrieving the returned items. + +You return in the response the retrieved brands and the pagination configurations. + +Learn more about pagination with Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#apply-pagination/index.html.md). + +*** + +## 2. Add Default Query Configurations + +Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. + +Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformQuery, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" +// other imports... + +export const GetBrandsSchema = createFindParams() + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/brands", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetBrandsSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + + ], +}) +``` + +You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: + +- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides `createFindParams` that generates a Zod schema with the following properties: + - `fields`: A comma-separated string indicating the fields to retrieve. + - `limit`: The maximum number of items to retrieve. + - `offset`: The number of items to skip before retrieving the returned items. + - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](https://docs.medusajs.com/api/admin#sort-order) +- An object of Query configurations having the following properties: + - `defaults`: An array of default fields and relations to retrieve. + - `isList`: Whether the API route returns a list of items. + +By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route. + +Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](https://docs.medusajs.com/learn/fundamentals/module-links/query#request-query-configurations/index.html.md). + +*** + +## 3. Initialize JS SDK + +In your custom UI route, you'll retrieve the brands 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 core API route. + +If you didn't follow the [previous chapter](https://docs.medusajs.com/learn/customization/customize-admin/widget/index.html.md), 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). + +*** + +## 4. Add a UI Route to Show Brands + +You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard. + +Learn more about UI routes in [this chapter](https://docs.medusajs.com/learn/fundamentals/admin/ui-routes/index.html.md). + +So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content: + +![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) + +```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights} +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { TagSolid } from "@medusajs/icons" +import { + Container, +} from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../../lib/sdk" +import { useMemo, useState } from "react" + +const BrandsPage = () => { + // TODO retrieve brands + + return ( + + {/* TODO show brands */} + + ) +} + +export const config = defineRouteConfig({ + label: "Brands", + icon: TagSolid, +}) + +export default BrandsPage +``` + +A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK. + +So far, you only show a container. 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. + +### Retrieve Brands From API Route + +You'll now update the UI route to retrieve the brands from the API route you added earlier. + +First, add the following type in `src/admin/routes/brands/page.tsx`: + +```tsx title="src/admin/routes/brands/page.tsx" +type Brand = { + id: string + name: string +} +type BrandsResponse = { + brands: Brand[] + count: number + limit: number + offset: number +} +``` + +You define the type for a brand, and the type of expected response from the `GET /admin/brands` API route. + +To display the brands, you'll use Medusa UI's [DataTable](https://docs.medusajs.com/ui/components/data-table/index.html.md) component. So, add the following imports in `src/admin/routes/brands/page.tsx`: + +```tsx title="src/admin/routes/brands/page.tsx" +import { + // ... + Heading, + createDataTableColumnHelper, + DataTable, + DataTablePaginationState, + useDataTable, +} from "@medusajs/ui" +``` + +You import the `DataTable` component and the following utilities: + +- `createDataTableColumnHelper`: A utility to create columns for the data table. +- `DataTablePaginationState`: A type that holds the pagination state of the data table. +- `useDataTable`: A hook to initialize and configure the data table. + +You also import the `Heading` component to show a heading above the data table. + +Next, you'll define the table's columns. Add the following before the `BrandsPage` component: + +```tsx title="src/admin/routes/brands/page.tsx" +const columnHelper = createDataTableColumnHelper() + +const columns = [ + columnHelper.accessor("id", { + header: "ID", + }), + columnHelper.accessor("name", { + header: "Name", + }), +] +``` + +You use the `createDataTableColumnHelper` utility to create columns for the data table. You define two columns for the ID and name of the brands. + +Then, replace the `// TODO retrieve brands` in the component with the following: + +```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} +const limit = 15 +const [pagination, setPagination] = useState({ + pageSize: limit, + pageIndex: 0, +}) +const offset = useMemo(() => { + return pagination.pageIndex * limit +}, [pagination]) + +const { data, isLoading } = useQuery({ + queryFn: () => sdk.client.fetch(`/admin/brands`, { + query: { + limit, + offset, + }, + }), + queryKey: [["brands", limit, offset]], +}) + +// TODO configure data table +``` + +To enable pagination in the `DataTable` component, you need to define a state variable of type `DataTablePaginationState`. It's an object having the following properties: + +- `pageSize`: The maximum number of items per page. You set it to `15`. +- `pageIndex`: A zero-based index of the current page of items. + +You also define a memoized `offset` value that indicates the number of items to skip before retrieving the current page's items. + +Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. + +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. + +In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property. + +This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page. + +### Display Brands Table + +Finally, you'll display the brands in a data table. Replace the `// TODO configure data table` in the component with the following: + +```tsx title="src/admin/routes/brands/page.tsx" +const table = useDataTable({ + columns, + data: data?.brands || [], + getRowId: (row) => row.id, + rowCount: data?.count || 0, + isLoading, + pagination: { + state: pagination, + onPaginationChange: setPagination, + }, +}) +``` + +You use the `useDataTable` hook to initialize and configure the data table. It accepts an object with the following properties: + +- `columns`: The columns of the data table. You created them using the `createDataTableColumnHelper` utility. +- `data`: The brands to display in the table. +- `getRowId`: A function that returns a unique identifier for a row. +- `rowCount`: The total count of items. This is used to determine the number of pages. +- `isLoading`: A boolean indicating whether the data is loading. +- `pagination`: An object to configure pagination. It accepts the following properties: + - `state`: The pagination state of the data table. + - `onPaginationChange`: A function to update the pagination state. + +Then, replace the `{/* TODO show brands */}` in the return statement with the following: + +```tsx title="src/admin/routes/brands/page.tsx" + + + Brands + + + + +``` + +This renders the data table that shows the brands with pagination. The `DataTable` component accepts the `instance` prop, which is the object returned by the `useDataTable` hook. + +*** + +## Test it Out + +To test out the UI route, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. + +![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) + +*** + +## Summary + +By following the previous chapters, you: + +- Injected a widget into the product details page to show the product's brand. +- Created a UI route in the Medusa Admin that shows the list of brands. + +*** + +## Next Steps: Integrate Third-Party Systems + +Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. + +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: 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 +} + +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: Sync Brands from Medusa to Third-Party + +In the [previous chapter](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. + +In another previous chapter, you [added a workflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. + +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. + +Learn more about Medusa's event system and subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. + +### Prerequisites + +- [createBrandWorkflow](https://docs.medusajs.com/learn/customization/custom-features/workflow/index.html.md) +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +## 1. Emit Event in createBrandWorkflow + +Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. + +Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: + +```ts title="src/workflows/create-brand.ts" highlights={eventHighlights} +// other imports... +import { + emitEventStep, +} from "@medusajs/medusa/core-flows" + +// ... + +export const createBrandWorkflow = createWorkflow( + "create-brand", + (input: CreateBrandInput) => { + // ... + + emitEventStep({ + eventName: "brand.created", + data: { + id: brand.id, + }, + }) + + return new WorkflowResponse(brand) + } +) +``` + +The `emitEventStep` accepts an object parameter having two properties: + +- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. +- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. + +You'll learn how to handle this event in a later step. + +*** + +## 2. Create Sync to Third-Party System Workflow + +The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. + +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +You'll create a `syncBrandToSystemWorkflow` that has two steps: + +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query/index.html.md). You'll use this to retrieve the brand's details using its ID. +- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. + +### syncBrandToCmsStep + +To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" +import { CMS_MODULE } from "../modules/cms" +import CmsModuleService from "../modules/cms/service" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf +} + +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.createBrand(brand) + + return new StepResponse(null, brand.id) + }, + async (id, { container }) => { + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) + + await cmsModuleService.deleteBrand(id) + } +) +``` + +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) and use its `createBrand` method. This method will create the brand in the third-party CMS. + +You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. + +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +### Create Workflow + +You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: + +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} +// other imports... +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +type SyncBrandToCmsWorkflowInput = { + id: string +} + +export const syncBrandToCmsWorkflow = createWorkflow( + "sync-brand-to-cms", + (input: SyncBrandToCmsWorkflowInput) => { + // @ts-ignore + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: ["*"], + filters: { + id: input.id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) + } +) +``` + +You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: + +- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. +- `syncBrandToCmsStep`: Create the brand in the third-party CMS. + +You'll execute this workflow in the subscriber next. + +Learn more about `useQueryGraphStep` in [this reference](https://docs.medusajs.com/resources/references/helper-steps/useQueryGraphStep/index.html.md). + +*** + +## 3. Handle brand.created Event + +You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: + +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) + +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} +import type { + SubscriberConfig, + SubscriberArgs, +} from "@medusajs/framework" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" + +export default async function brandCreatedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await syncBrandToCmsWorkflow(container).run({ + input: data, + }) +} + +export const config: SubscriberConfig = { + event: "brand.created", +} +``` + +A subscriber file must export: + +- The asynchronous function that's executed when the event is emitted. This must be the file's default export. +- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. + +The subscriber function accepts an object parameter that has two properties: + +- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. +- `container`: The Medusa container used to resolve Framework and commerce tools. + +In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. + +Learn more about subscribers in [this chapter](https://docs.medusajs.com/learn/fundamentals/events-and-subscribers/index.html.md). + +*** + +## Test it Out + +To test the subscriber and workflow out, you'll use the [Create Brand API route](https://docs.medusajs.com/learn/customization/custom-features/api-route/index.html.md) you created in a previous chapter. + +First, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: + +```bash +curl -X POST 'http://localhost:9000/auth/user/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +Make sure to replace the email and password with your admin user's credentials. + +Don't have an admin user? Refer to [this guide](https://docs.medusajs.com/learn/installation#create-medusa-admin-user/index.html.md). + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: + +```plain +info: Processing brand.created which has 1 subscribers +http: POST /admin/brands ← - (200) - 16.418 ms +info: Sending a POST request to /brands. +info: Request Data: { + "id": "01JEDWENYD361P664WRQPMC3J8", + "name": "Acme", + "created_at": "2024-12-06T11:42:32.909Z", + "updated_at": "2024-12-06T11:42:32.909Z", + "deleted_at": null +} +info: API Key: "123" +``` + +*** + +## Next Chapter: Sync Brand from Third-Party CMS to Medusa + +You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. + + +# Guide: Schedule Syncing Brands from Third-Party + +In the previous chapters, you've [integrated a third-party CMS](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) and implemented the logic to [sync created brands](https://docs.medusajs.com/learn/customization/integrate-systems/handle-event/index.html.md) from Medusa to the CMS. + +However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. + +You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. + +Learn more about scheduled jobs in [this chapter](https://docs.medusajs.com/learn/fundamentals/scheduled-jobs/index.html.md). + +In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. + +### Prerequisites + +- [CMS Module](https://docs.medusajs.com/learn/customization/integrate-systems/service/index.html.md) + +*** + +## 1. Implement Syncing Workflow + +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. + +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. + +Learn more about workflows in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/index.html.md). + +This workflow will have three steps: + +1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. +2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. +3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. + +### retrieveBrandsFromCmsStep + +To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: + +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) + +```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import CmsModuleService from "../modules/cms/service" +import { CMS_MODULE } from "../modules/cms" + +const retrieveBrandsFromCmsStep = createStep( + "retrieve-brands-from-cms", + async (_, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve( + CMS_MODULE + ) + + const brands = await cmsModuleService.retrieveBrands() + + return new StepResponse(brands) + } +) +``` + +You create a `retrieveBrandsFromCmsStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. + +### createBrandsStep + +The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +// other imports... +import BrandModuleService from "../modules/brand/service" +import { BRAND_MODULE } from "../modules/brand" + +// ... + +type CreateBrand = { + name: string +} + +type CreateBrandsInput = { + brands: CreateBrand[] +} + +export const createBrandsStep = createStep( + "create-brands-step", + async (input: CreateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const brands = await brandModuleService.createBrands(input.brands) + + return new StepResponse(brands, brands) + }, + async (brands, { container }) => { + if (!brands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) + } +) +``` + +The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](https://docs.medusajs.com/learn/customization/custom-features/module/index.html.md)'s service and uses the generated `createBrands` method to create the brands. + +The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. + +Learn more about compensation functions in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/compensation-function/index.html.md). + +### Update Brands Step + +The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: + +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} +// ... + +type UpdateBrand = { + id: string + name: string +} + +type UpdateBrandsInput = { + brands: UpdateBrand[] +} + +export const updateBrandsStep = createStep( + "update-brands-step", + async ({ brands }: UpdateBrandsInput, { container }) => { + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + const prevUpdatedBrands = await brandModuleService.listBrands({ + id: brands.map((brand) => brand.id), + }) + + const updatedBrands = await brandModuleService.updateBrands(brands) + + return new StepResponse(updatedBrands, prevUpdatedBrands) + }, + async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } + + const brandModuleService: BrandModuleService = container.resolve( + BRAND_MODULE + ) + + await brandModuleService.updateBrands(prevUpdatedBrands) + } +) +``` + +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. + +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. + +### Create Workflow + +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... +import { + // ... + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +// ... + +export const syncBrandsFromCmsWorkflow = createWorkflow( + "sync-brands-from-system", + () => { + const brands = retrieveBrandsFromCmsStep() + + // TODO create and update brands + } +) +``` + +In the workflow, you only use the `retrieveBrandsFromCmsStep` for now, which retrieves the brands from the third-party CMS. + +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. + +Learn more about data manipulation using `transform` in [this chapter](https://docs.medusajs.com/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +So, replace the `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const { toCreate, toUpdate } = transform( + { + brands, + }, + (data) => { + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] + + data.brands.forEach((brand) => { + if (brand.external_id) { + toUpdate.push({ + id: brand.external_id as string, + name: brand.name as string, + }) + } else { + toCreate.push({ + name: brand.name as string, + }) + } + }) + + return { toCreate, toUpdate } + } +) + +// TODO create and update the brands +``` + +`transform` accepts two parameters: + +1. The data to be passed to the function in the second parameter. +2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. + +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. + +You now have the list of brands to create and update. So, replace the new `TODO` with the following: + +```ts title="src/workflows/sync-brands-from-cms.ts" +const created = createBrandsStep({ brands: toCreate }) +const updated = updateBrandsStep({ brands: toUpdate }) + +return new WorkflowResponse({ + created, + updated, +}) +``` + +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. + +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. + +*** + +## 2. Schedule Syncing Task + +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. + +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) + +```ts title="src/jobs/sync-brands-from-cms.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" + +export default async function (container: MedusaContainer) { + const logger = container.resolve("logger") + + const { result } = await syncBrandsFromCmsWorkflow(container).run() + + logger.info( + `Synced brands from third-party system: ${ + result.created.length + } brands created and ${result.updated.length} brands updated.`) +} + +export const config = { + name: "sync-brands-from-system", + schedule: "0 0 * * *", // change to * * * * * for debugging +} +``` + +A scheduled job file must export: + +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. + +The scheduled job function accepts as a parameter the [Medusa container](https://docs.medusajs.com/learn/fundamentals/medusa-container/index.html.md) used to resolve Framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. + +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. + +*** + +## Test it Out + +To test out the scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. + +*** + +## Summary + +By following the previous chapters, you utilized the Medusa Framework and orchestration tools to perform and automate tasks that span across systems. + +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 + +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. + +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. + +![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) + +*** + +## 2. Create Module Service + +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. + +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: + +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) + +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} +import { Logger, ConfigModule } from "@medusajs/framework/types" + +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 +``` + +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, +}) +``` + +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`. + +*** + +## 4. Add Module to Medusa's Configurations + +Finally, add the module to the Medusa configurations at `medusa-config.ts`: + +```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 +CMS_API_KEY=123 +``` + +*** + +## Next Steps: Sync Brand From Medusa to CMS + +You can now use the CMS Module's service to perform actions on the third-party CMS. + +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. + + # 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. @@ -17826,6 +17826,291 @@ 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. +# 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. + +### Setup the Documentation Workspace + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Yarn v3+](https://v3.yarnpkg.com/getting-started/install) + +In the `www` directory, run the following command to install the dependencies: + +```bash +yarn install +``` + +Then, run the following command to build packages under the `www/packages` directory: + +```bash +yarn build +``` + +After that, you can change into the directory of any documentation project under the `www/apps` directory and run the `dev` command to start the development server. + +*** + +## 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 API Routes In this chapter, you'll learn how to write integration tests for API routes using [medusaIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/integration-tests/index.html.md) from Medusa's Testing Framework. @@ -18395,268 +18680,74 @@ const response = await api.post(`/custom`, form, { ``` -# Docs Contribution Guidelines +# Example: Integration Tests for a Module -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. +In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. -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). +### Prerequisites -## What Can You Contribute? +- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) -You can contribute to the Medusa documentation in the following ways: +## Write Integration Test for Module -- 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. +Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: -*** +```ts title="src/modules/blog/service.ts" +import { MedusaService } from "@medusajs/framework/utils" +import MyCustom from "./models/my-custom" -## Documentation Workspace +class BlogModuleService extends MedusaService({ + MyCustom, +}){ + getMessage(): string { + return "Hello, World!" + } +} -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 +export default BlogModuleService ``` -```` -The code snippet must be written using NPM. +To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: -### Global Option +```ts title="src/modules/blog/__tests__/service.spec.ts" +import { moduleIntegrationTestRunner } from "@medusajs/test-utils" +import { BLOG_MODULE } from ".." +import BlogModuleService from "../service" +import MyCustom from "../models/my-custom" -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: +moduleIntegrationTestRunner({ + moduleName: BLOG_MODULE, + moduleModels: [MyCustom], + resolve: "./src/modules/blog", + testSuite: ({ service }) => { + describe("BlogModuleService", () => { + it("says hello world", () => { + const message = service.getMessage() + + expect(message).toEqual("Hello, World!") + }) + }) + }, +}) + +jest.setTimeout(60 * 1000) +``` + +You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. + +*** + +## Run Test + +Run the following command to run your module integration tests: ```bash npm2yarn -npm install @medusajs/cli -g +npm run test:integration:modules ``` -*** +If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). -## 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") -``` -~~~ */} +This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. # Example: Write Integration Tests for Workflows @@ -18790,76 +18881,6 @@ The `errors` property contains an array of errors thrown during the execution of If you threw a `MedusaError`, then you can check the error message in `errors[0].error.message`. -# Example: Integration Tests for a Module - -In this chapter, find an example of writing an integration test for a module using [moduleIntegrationTestRunner](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/modules-tests/index.html.md) from Medusa's Testing Framework. - -### Prerequisites - -- [Testing Tools Setup](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools/index.html.md) - -## Write Integration Test for Module - -Consider a `blog` module with a `BlogModuleService` that has a `getMessage` method: - -```ts title="src/modules/blog/service.ts" -import { MedusaService } from "@medusajs/framework/utils" -import MyCustom from "./models/my-custom" - -class BlogModuleService extends MedusaService({ - MyCustom, -}){ - getMessage(): string { - return "Hello, World!" - } -} - -export default BlogModuleService -``` - -To create an integration test for the method, create the file `src/modules/blog/__tests__/service.spec.ts` with the following content: - -```ts title="src/modules/blog/__tests__/service.spec.ts" -import { moduleIntegrationTestRunner } from "@medusajs/test-utils" -import { BLOG_MODULE } from ".." -import BlogModuleService from "../service" -import MyCustom from "../models/my-custom" - -moduleIntegrationTestRunner({ - moduleName: BLOG_MODULE, - moduleModels: [MyCustom], - resolve: "./src/modules/blog", - testSuite: ({ service }) => { - describe("BlogModuleService", () => { - it("says hello world", () => { - const message = service.getMessage() - - expect(message).toEqual("Hello, World!") - }) - }) - }, -}) - -jest.setTimeout(60 * 1000) -``` - -You use the `moduleIntegrationTestRunner` function to add tests for the `blog` module. You have one test that passes if the `getMessage` method returns the `"Hello, World!"` string. - -*** - -## Run Test - -Run the following command to run your module integration tests: - -```bash npm2yarn -npm run test:integration:modules -``` - -If you don't have a `test:integration:modules` script in `package.json`, refer to the [Medusa Testing Tools chapter](https://docs.medusajs.com/learn/debugging-and-testing/testing-tools#add-test-commands/index.html.md). - -This runs your Medusa application and runs the tests available in any `__tests__` directory under the `src/modules` directory. - - # Commerce Modules In this section of the documentation, you'll find guides and references related to Medusa's Commerce Modules. @@ -19040,154 +19061,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. @@ -19329,286 +19202,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Cart Module - -In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. - -Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Cart Features - -- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. -- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. -- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. -- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. - -*** - -## How to Use the Cart Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/create-cart.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createCartStep = createStep( - "create-cart", - async ({}, { container }) => { - const cartModuleService = container.resolve(Modules.CART) - - const cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - address_1: "1512 Barataria Blvd", - country_code: "us", - }, - items: [ - { - title: "Shirt", - unit_price: 1000, - quantity: 1, - }, - ], - }) - - return new StepResponse({ cart }, cart.id) - }, - async (cartId, { container }) => { - if (!cartId) { - return - } - const cartModuleService = container.resolve(Modules.CART) - - await cartModuleService.deleteCarts([cartId]) - } -) - -export const createCartWorkflow = createWorkflow( - "create-cart", - () => { - const { cart } = createCartStep() - - return new WorkflowResponse({ - cart, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -### API Route - -```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { createCartWorkflow } from "../../workflows/create-cart" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createCartWorkflow(req.scope) - .run() - - res.send(result) -} -``` - -### Subscriber - -```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - type SubscriberConfig, - type SubscriberArgs, -} from "@medusajs/framework" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config: SubscriberConfig = { - event: "user.created", -} -``` - -### Scheduled Job - -```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} -import { MedusaContainer } from "@medusajs/framework/types" -import { createCartWorkflow } from "../workflows/create-cart" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createCartWorkflow(container) - .run() - - console.log(result) -} - -export const config = { - name: "run-once-a-day", - schedule: `0 0 * * *`, -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - - -# Auth Module - -In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. - -Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## Auth Features - -- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. -- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). -- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. -- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. - -*** - -## How to Use the Auth Module - -In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. - -For example: - -```ts title="src/workflows/authenticate-user.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules, MedusaError } from "@medusajs/framework/utils" -import { MedusaRequest } from "@medusajs/framework/http" -import { AuthenticationInput } from "@medusajs/framework/types" - -type Input = { - req: MedusaRequest -} - -const authenticateUserStep = createStep( - "authenticate-user", - async ({ req }: Input, { container }) => { - const authModuleService = container.resolve(Modules.AUTH) - - const { success, authIdentity, error } = await authModuleService - .authenticate( - "emailpass", - { - url: req.url, - headers: req.headers, - query: req.query, - body: req.body, - authScope: "admin", // or custom actor type - protocol: req.protocol, - } as AuthenticationInput - ) - - if (!success) { - // incorrect authentication details - throw new MedusaError( - MedusaError.Types.UNAUTHORIZED, - error || "Incorrect authentication details" - ) - } - - return new StepResponse({ authIdentity }, authIdentity?.id) - }, - async (authIdentityId, { container }) => { - if (!authIdentityId) { - return - } - - const authModuleService = container.resolve(Modules.AUTH) - - await authModuleService.deleteAuthIdentities([authIdentityId]) - } -) - -export const authenticateUserWorkflow = createWorkflow( - "authenticate-user", - (input: Input) => { - const { authIdentity } = authenticateUserStep(input) - - return new WorkflowResponse({ - authIdentity, - }) - } -) -``` - -You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: - -```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { authenticateUserWorkflow } from "../../workflows/authenticate-user" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await authenticateUserWorkflow(req.scope) - .run({ - req, - }) - - res.send(result) -} -``` - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -*** - -## Configure Auth Module - -The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. - -*** - -## Providers - -Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. - -*** - - # Fulfillment Module In this section of the documentation, you will find resources to learn more about the Fulfillment Module and how to use it in your application. @@ -20076,27 +19669,24 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# Pricing Module +# Currency Module -In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Currency Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. +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 pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. +Medusa has 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). -## Pricing Features +## Currency Features -- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. -- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. -- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. -- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. -- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. +- [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 Pricing 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. @@ -20104,55 +19694,46 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-price-set.ts" highlights={highlights} +```ts title="src/workflows/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 createPriceSetStep = createStep( - "create-price-set", +const retrieveCurrencyStep = createStep( + "retrieve-currency", async ({}, { container }) => { - const pricingModuleService = container.resolve(Modules.PRICING) + const currencyModuleService = container.resolve(Modules.CURRENCY) - const priceSet = await pricingModuleService.createPriceSets({ - prices: [ - { - amount: 500, - currency_code: "USD", - }, - { - amount: 400, - currency_code: "EUR", - min_quantity: 0, - max_quantity: 4, - rules: {}, - }, - ], - }) + const currency = await currencyModuleService + .retrieveCurrency("usd") - return new StepResponse({ priceSet }, priceSet.id) - }, - async (priceSetId, { container }) => { - if (!priceSetId) { - return - } - const pricingModuleService = container.resolve(Modules.PRICING) - - await pricingModuleService.deletePriceSets([priceSetId]) + return new StepResponse({ currency }) } ) -export const createPriceSetWorkflow = createWorkflow( - "create-price-set", - () => { - const { priceSet } = createPriceSetStep() +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({ - priceSet, + formattedPrice, }) } ) @@ -20162,19 +19743,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 { createPriceSetWorkflow } from "../../workflows/create-price-set" +import { retrievePriceWithCurrency } from "../../workflows/retrieve-price-with-currency" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPriceSetWorkflow(req.scope) - .run() + const { result } = await retrievePriceWithCurrency(req.scope) + .run({ + price: 10, + }) res.send(result) } @@ -20182,19 +19765,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 { createPriceSetWorkflow } from "../workflows/create-price-set" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPriceSetWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -20206,15 +19791,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 { createPriceSetWorkflow } from "../workflows/create-price-set" +import { retrievePriceWithCurrency } from "../workflows/retrieve-price-with-currency" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPriceSetWorkflow(container) - .run() + const { result } = await retrievePriceWithCurrency(container) + .run({ + price: 10, + }) console.log(result) } @@ -20385,26 +19972,27 @@ Medusa provides the following payment providers out-of-the-box. You can use them *** -# Promotion Module +# Pricing Module -In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. +In this section of the documentation, you will find resources to learn more about the Pricing Module and how to use it in your application. -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/price-lists/index.html.md) to learn how to manage price lists using the dashboard. -Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. +Medusa has pricing related features available out-of-the-box through the Pricing Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Pricing Module. Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). -## Promotion Features +## Pricing Features -- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. -- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. -- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. -- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. +- [Price Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts/index.html.md): Store and manage prices of a resource, such as a product or a variant. +- [Advanced Rule Engine](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-rules/index.html.md): Create prices with tiers and custom rules to condition prices based on different contexts. +- [Price Lists](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/concepts#price-list/index.html.md): Group prices and apply them only in specific conditions with price lists. +- [Price Calculation Strategy](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md): Retrieve the best price in a given context and for the specified rule values. +- [Tax-Inclusive Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/index.html.md): Calculate prices with taxes included in the price, and Medusa will handle calculating the taxes automatically. *** -## How to Use the Promotion Module +## How to Use the Pricing Module In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. @@ -20412,7 +20000,7 @@ You can build custom workflows and steps. You can also re-use Medusa's workflows For example: -```ts title="src/workflows/create-promotion.ts" highlights={highlights} +```ts title="src/workflows/create-price-set.ts" highlights={highlights} import { createWorkflow, WorkflowResponse, @@ -20421,41 +20009,46 @@ import { } from "@medusajs/framework/workflows-sdk" import { Modules } from "@medusajs/framework/utils" -const createPromotionStep = createStep( - "create-promotion", +const createPriceSetStep = createStep( + "create-price-set", async ({}, { container }) => { - const promotionModuleService = container.resolve(Modules.PROMOTION) + const pricingModuleService = container.resolve(Modules.PRICING) - const promotion = await promotionModuleService.createPromotions({ - code: "10%OFF", - type: "standard", - application_method: { - type: "percentage", - target_type: "order", - value: 10, - currency_code: "usd", - }, + const priceSet = await pricingModuleService.createPriceSets({ + prices: [ + { + amount: 500, + currency_code: "USD", + }, + { + amount: 400, + currency_code: "EUR", + min_quantity: 0, + max_quantity: 4, + rules: {}, + }, + ], }) - return new StepResponse({ promotion }, promotion.id) + return new StepResponse({ priceSet }, priceSet.id) }, - async (promotionId, { container }) => { - if (!promotionId) { + async (priceSetId, { container }) => { + if (!priceSetId) { return } - const promotionModuleService = container.resolve(Modules.PROMOTION) + const pricingModuleService = container.resolve(Modules.PRICING) - await promotionModuleService.deletePromotions(promotionId) + await pricingModuleService.deletePriceSets([priceSetId]) } ) -export const createPromotionWorkflow = createWorkflow( - "create-promotion", +export const createPriceSetWorkflow = createWorkflow( + "create-price-set", () => { - const { promotion } = createPromotionStep() + const { priceSet } = createPriceSetStep() return new WorkflowResponse({ - promotion, + priceSet, }) } ) @@ -20470,13 +20063,13 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { createPromotionWorkflow } from "../../workflows/create-cart" +import { createPriceSetWorkflow } from "../../workflows/create-price-set" export async function GET( req: MedusaRequest, res: MedusaResponse ) { - const { result } = await createPromotionWorkflow(req.scope) + const { result } = await createPriceSetWorkflow(req.scope) .run() res.send(result) @@ -20490,13 +20083,13 @@ import { type SubscriberConfig, type SubscriberArgs, } from "@medusajs/framework" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function handleUserCreated({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -20511,12 +20104,12 @@ export const config: SubscriberConfig = { ```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} import { MedusaContainer } from "@medusajs/framework/types" -import { createPromotionWorkflow } from "../workflows/create-cart" +import { createPriceSetWorkflow } from "../workflows/create-price-set" export default async function myCustomJob( container: MedusaContainer ) { - const { result } = await createPromotionWorkflow(container) + const { result } = await createPriceSetWorkflow(container) .run() console.log(result) @@ -20831,6 +20424,154 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Promotion Module + +In this section of the documentation, you will find resources to learn more about the Promotion Module and how to use it in your application. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/promotions/index.html.md) to learn how to manage promotions using the dashboard. + +Medusa has promotion related features available out-of-the-box through the Promotion Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Promotion Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Promotion Features + +- [Discount Functionalities](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts/index.html.md): A promotion discounts an amount or percentage of a cart's items, shipping methods, or the entire order. +- [Flexible Promotion Rules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/concepts#flexible-rules/index.html.md): A promotion has rules that restricts when the promotion is applied. +- [Campaign Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/campaign/index.html.md): A campaign combines promotions under the same conditions, such as start and end dates, and budget configurations. +- [Apply Promotion on Carts and Orders](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md): Apply promotions on carts and orders to discount items, shipping methods, or the entire order. + +*** + +## How to Use the Promotion Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +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-promotion.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createPromotionStep = createStep( + "create-promotion", + async ({}, { container }) => { + const promotionModuleService = container.resolve(Modules.PROMOTION) + + const promotion = await promotionModuleService.createPromotions({ + code: "10%OFF", + type: "standard", + application_method: { + type: "percentage", + target_type: "order", + value: 10, + currency_code: "usd", + }, + }) + + return new StepResponse({ promotion }, promotion.id) + }, + async (promotionId, { container }) => { + if (!promotionId) { + return + } + const promotionModuleService = container.resolve(Modules.PROMOTION) + + await promotionModuleService.deletePromotions(promotionId) + } +) + +export const createPromotionWorkflow = createWorkflow( + "create-promotion", + () => { + const { promotion } = createPromotionStep() + + return new WorkflowResponse({ + promotion, + }) + } +) +``` + +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 { createPromotionWorkflow } from "../../workflows/create-cart" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createPromotionWorkflow(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 { createPromotionWorkflow } from "../workflows/create-cart" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createPromotionWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createPromotionWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createPromotionWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Sales Channel Module In this section of the documentation, you will find resources to learn more about the Sales Channel Module and how to use it in your application. @@ -20991,6 +20732,136 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** +# Auth Module + +In this section of the documentation, you will find resources to learn more about the Auth Module and how to use it in your application. + +Medusa has auth related features available out-of-the-box through the Auth Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Auth Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Auth Features + +- [Basic User Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#1-basic-authentication-flow/index.html.md): Authenticate users using their email and password credentials. +- [Third-Party and Social Authentication](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md): Authenticate users using third-party services and social platforms, such as [Google](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/google/index.html.md) and [GitHub](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/github/index.html.md). +- [Authenticate Custom Actor Types](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md): Create custom user or actor types, such as managers, authenticate them in your application, and guard routes based on the custom user types. +- [Custom Authentication Providers](https://docs.medusajs.com/references/auth/provider/index.html.md): Integrate third-party services with custom authentication providors. + +*** + +## How to Use the Auth Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/authenticate-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules, MedusaError } from "@medusajs/framework/utils" +import { MedusaRequest } from "@medusajs/framework/http" +import { AuthenticationInput } from "@medusajs/framework/types" + +type Input = { + req: MedusaRequest +} + +const authenticateUserStep = createStep( + "authenticate-user", + async ({ req }: Input, { container }) => { + const authModuleService = container.resolve(Modules.AUTH) + + const { success, authIdentity, error } = await authModuleService + .authenticate( + "emailpass", + { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: "admin", // or custom actor type + protocol: req.protocol, + } as AuthenticationInput + ) + + if (!success) { + // incorrect authentication details + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Incorrect authentication details" + ) + } + + return new StepResponse({ authIdentity }, authIdentity?.id) + }, + async (authIdentityId, { container }) => { + if (!authIdentityId) { + return + } + + const authModuleService = container.resolve(Modules.AUTH) + + await authModuleService.deleteAuthIdentities([authIdentityId]) + } +) + +export const authenticateUserWorkflow = createWorkflow( + "authenticate-user", + (input: Input) => { + const { authIdentity } = authenticateUserStep(input) + + return new WorkflowResponse({ + authIdentity, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +```ts title="API Route" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { authenticateUserWorkflow } from "../../workflows/authenticate-user" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await authenticateUserWorkflow(req.scope) + .run({ + req, + }) + + res.send(result) +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + +## Configure Auth Module + +The Auth Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/module-options/index.html.md) for details on the module's options. + +*** + +## Providers + +Medusa provides the following authentication providers out-of-the-box. You can use them to authenticate admin users, customers, or custom actor types. + +*** + + # Stock Location Module In this section of the documentation, you will find resources to learn more about the Stock Location Module and how to use it in your application. @@ -21128,153 +20999,6 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc *** -# 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. - -Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. - -Medusa has user related features available out-of-the-box through the User Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this User Module. - -Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). - -## User Features - -- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. -- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. - -*** - -## How to Use User 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-user.ts" highlights={highlights} -import { - createWorkflow, - WorkflowResponse, - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" - -const createUserStep = createStep( - "create-user", - async ({}, { container }) => { - const userModuleService = container.resolve(Modules.USER) - - const user = await userModuleService.createUsers({ - email: "user@example.com", - first_name: "John", - last_name: "Smith", - }) - - return new StepResponse({ user }, user.id) - }, - async (userId, { container }) => { - if (!userId) { - return - } - const userModuleService = container.resolve(Modules.USER) - - await userModuleService.deleteUsers([userId]) - } -) - -export const createUserWorkflow = createWorkflow( - "create-user", - () => { - const { user } = createUserStep() - - return new WorkflowResponse({ - user, - }) - } -) -``` - -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 { createUserWorkflow } from "../../workflows/create-user" - -export async function GET( - req: MedusaRequest, - res: MedusaResponse -) { - const { result } = await createUserWorkflow(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 { createUserWorkflow } from "../workflows/create-user" - -export default async function handleUserCreated({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const { result } = await createUserWorkflow(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 { createUserWorkflow } from "../workflows/create-user" - -export default async function myCustomJob( - container: MedusaContainer -) { - const { result } = await createUserWorkflow(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 User Module - -The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. - -*** - - # Store Module In this section of the documentation, you will find resources to learn more about the Store Module and how to use it in your application. @@ -21561,6 +21285,181 @@ The Tax Module accepts options for further configurations. Refer to [this docume *** +# 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. + +Refer to the [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/users/index.html.md) to learn how to manage users using the dashboard. + +Medusa has user related features available out-of-the-box through the User Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this User Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## User Features + +- [User Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows/index.html.md): Store and manage users in your store. +- [Invite Users](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/user-creation-flows#invite-users/index.html.md): Invite users to join your store and manage those invites. + +*** + +## How to Use User 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-user.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createUserStep = createStep( + "create-user", + async ({}, { container }) => { + const userModuleService = container.resolve(Modules.USER) + + const user = await userModuleService.createUsers({ + email: "user@example.com", + first_name: "John", + last_name: "Smith", + }) + + return new StepResponse({ user }, user.id) + }, + async (userId, { container }) => { + if (!userId) { + return + } + const userModuleService = container.resolve(Modules.USER) + + await userModuleService.deleteUsers([userId]) + } +) + +export const createUserWorkflow = createWorkflow( + "create-user", + () => { + const { user } = createUserStep() + + return new WorkflowResponse({ + user, + }) + } +) +``` + +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 { createUserWorkflow } from "../../workflows/create-user" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createUserWorkflow(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 { createUserWorkflow } from "../workflows/create-user" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createUserWorkflow(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 { createUserWorkflow } from "../workflows/create-user" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createUserWorkflow(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 User Module + +The User Module accepts options for further configurations. Refer to [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/user/module-options/index.html.md) for details on the module's options. + +*** + + +# API Key Concepts + +In this document, you’ll learn about the different types of API keys, their expiration and verification. + +## API Key Types + +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. + + # Links between API Key Module and Other Modules This document showcases the module links defined between the API Key Module and other Commerce Modules. @@ -21659,91 +21558,6 @@ createRemoteLinkStep({ ``` -# API Key Concepts - -In this document, you’ll learn about the different types of API keys, their expiration and verification. - -## API Key Types - -There are two types of API keys: - -- `publishable`: A public key used in client applications, such as a storefront. -- `secret`: A secret key used for authentication and verification purposes, such as an admin user’s authentication token or a password reset token. - -The API key’s type is stored in the `type` property of the [ApiKey data model](https://docs.medusajs.com/references/api-key/models/ApiKey/index.html.md). - -*** - -## API Key Expiration - -An API key expires when it’s revoked using the [revoke method of the module’s main service](https://docs.medusajs.com/references/api-key/revoke/index.html.md). - -The associated token is no longer usable or verifiable. - -*** - -## Token Verification - -To verify a token received as an input or in a request, use the [authenticate method of the module’s main service](https://docs.medusajs.com/references/api-key/authenticate/index.html.md) which validates the token against all non-expired tokens. - - -# 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. @@ -21767,6 +21581,156 @@ The above behavior means that two `Customer` records may exist with the same ema So, there can only be one guest customer (having `has_account=false`) and one registered customer (having `has_account=true`) with the same email. +# Cart Module + +In this section of the documentation, you will find resources to learn more about the Cart Module and how to use it in your application. + +Medusa has cart related features available out-of-the-box through the Cart Module. A [module](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) is a standalone package that provides features for a single domain. Each of Medusa's commerce features are placed in Commerce Modules, such as this Cart Module. + +Learn more about why modules are isolated in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/isolation/index.html.md). + +## Cart Features + +- [Cart Management](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/concepts/index.html.md): Store and manage carts, including their addresses, line items, shipping methods, and more. +- [Apply Promotion Adjustments](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/promotions/index.html.md): Apply promotions or discounts to line items and shipping methods by adding adjustment lines that are factored into their subtotals. +- [Apply Tax Lines](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/tax-lines/index.html.md): Apply tax lines to line items and shipping methods. +- [Cart Scoping](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/links-to-other-modules/index.html.md): When used in the Medusa application, Medusa creates links to other Commerce Modules, scoping a cart to a sales channel, region, and a customer. + +*** + +## How to Use the Cart Module + +In your Medusa application, you build flows around Commerce Modules. A flow is built as a [Workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md), which is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +You can build custom workflows and steps. You can also re-use Medusa's workflows and steps, which are provided by the `@medusajs/medusa/core-flows` package. + +For example: + +```ts title="src/workflows/create-cart.ts" highlights={highlights} +import { + createWorkflow, + WorkflowResponse, + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" + +const createCartStep = createStep( + "create-cart", + async ({}, { container }) => { + const cartModuleService = container.resolve(Modules.CART) + + const cart = await cartModuleService.createCarts({ + currency_code: "usd", + shipping_address: { + address_1: "1512 Barataria Blvd", + country_code: "us", + }, + items: [ + { + title: "Shirt", + unit_price: 1000, + quantity: 1, + }, + ], + }) + + return new StepResponse({ cart }, cart.id) + }, + async (cartId, { container }) => { + if (!cartId) { + return + } + const cartModuleService = container.resolve(Modules.CART) + + await cartModuleService.deleteCarts([cartId]) + } +) + +export const createCartWorkflow = createWorkflow( + "create-cart", + () => { + const { cart } = createCartStep() + + return new WorkflowResponse({ + cart, + }) + } +) +``` + +You can then execute the workflow in your custom API routes, scheduled jobs, or subscribers: + +### API Route + +```ts title="src/api/workflow/route.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { createCartWorkflow } from "../../workflows/create-cart" + +export async function GET( + req: MedusaRequest, + res: MedusaResponse +) { + const { result } = await createCartWorkflow(req.scope) + .run() + + res.send(result) +} +``` + +### Subscriber + +```ts title="src/subscribers/user-created.ts" highlights={[["11"], ["12"]]} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + type SubscriberConfig, + type SubscriberArgs, +} from "@medusajs/framework" +import { createCartWorkflow } from "../workflows/create-cart" + +export default async function handleUserCreated({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config: SubscriberConfig = { + event: "user.created", +} +``` + +### Scheduled Job + +```ts title="src/jobs/run-daily.ts" highlights={[["7"], ["8"]]} +import { MedusaContainer } from "@medusajs/framework/types" +import { createCartWorkflow } from "../workflows/create-cart" + +export default async function myCustomJob( + container: MedusaContainer +) { + const { result } = await createCartWorkflow(container) + .run() + + console.log(result) +} + +export const config = { + name: "run-once-a-day", + schedule: `0 0 * * *`, +} +``` + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). + +*** + + # Links between Customer Module and Other Modules This document showcases the module links defined between the Customer Module and other Commerce Modules. @@ -21944,1921 +21908,6 @@ const { data: orders } = useQueryGraphStep({ ``` -# 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. - - -# Tax Lines in Cart Module - -In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. - -## What are Tax Lines? - -A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. - -![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) - -*** - -## Tax Inclusivity - -By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. - -However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. - -So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. - -The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. - -![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) - -For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. - -*** - -## Retrieve Tax Lines - -When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. - -```ts -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.tax_lines", - "shipping_methods.tax_lines", - "shipping_address", - ], -}) - -// retrieve the tax lines -const taxLines = await taxModuleService.getTaxLines( - [ - ...(cart.items as TaxableItemDTO[]), - ...(cart.shipping_methods as TaxableShippingDTO[]), - ], - { - address: { - ...cart.shipping_address, - country_code: - cart.shipping_address.country_code || "us", - }, - } -) -``` - -Then, use the returned tax lines to set the line items and shipping methods’ tax lines: - -```ts -// set line item tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "line_item_id" in line) -) - -// set shipping method tax lines -await cartModuleService.setLineItemTaxLines( - cart.id, - taxLines.filter((line) => "shipping_line_id" in line) -) -``` - - -# Links between Cart Module and Other Modules - -This document showcases the module links defined between the Cart Module and other Commerce Modules. - -## Summary - -The Cart Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|Customer|Read-only - has one|Learn more| -|Order|Cart|Stored - one-to-one|Learn more| -|Cart|PaymentCollection|Stored - one-to-one|Learn more| -|LineItem|Product|Read-only - has one|Learn more| -|LineItem|ProductVariant|Read-only - has one|Learn more| -|Cart|Promotion|Stored - many-to-many|Learn more| -|Cart|Region|Read-only - has one|Learn more| -|Cart|SalesChannel|Read-only - has one|Learn more| - -*** - -## Customer Module - -Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. - -### 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[0].customer -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "customer.*", - ], -}) - -// carts[0].customer -``` - -*** - -## 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 features. - -Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. - -![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) - -### Retrieve with Query - -To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts[0].order -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "order.*", - ], -}) - -// carts[0].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.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.ORDER]: { - order_id: "order_123", - }, -}) -``` - -*** - -## Payment Module - -The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. - -Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. - -![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) - -### Retrieve with Query - -To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts[0].payment_collection -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "payment_collection.*", - ], -}) - -// carts[0].payment_collection -``` - -### 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", - }, -}) -``` - -*** - -## Product Module - -Medusa defines read-only links between: - -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. -- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. - -### Retrieve with Query - -To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: - -To retrieve the product, pass `product.*` in `fields`. - -### query.graph - -```ts -const { data: lineItems } = await query.graph({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: lineItems } = useQueryGraphStep({ - entity: "line_item", - fields: [ - "variant.*", - ], -}) - -// lineItems.variant -``` - -*** - -## Promotion Module - -The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. - -Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. - -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. - -### Retrieve with Query - -To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts[0].promotions -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "promotions.*", - ], -}) - -// carts[0].promotions -``` - -### Manage with Link - -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Region Module - -Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -*** - -## Sales Channel Module - -Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. - -### Retrieve with Query - -To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts[0].sales_channel -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "sales_channel.*", - ], -}) - -// carts[0].sales_channel -``` - - -# Promotions Adjustments in Carts - -In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. - -## What are Adjustment Lines? - -An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. - -The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. - -![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) - -The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. - -*** - -## Discountable Option - -The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. - -When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. - -*** - -## Promotion Actions - -When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. - -Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). - -For example: - -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - ComputeActionAdjustmentLine, - ComputeActionItemLine, - ComputeActionShippingLine, - // ... -} from "@medusajs/framework/types" - -// retrieve the cart -const cart = await cartModuleService.retrieveCart("cart_123", { - relations: [ - "items.adjustments", - "shipping_methods.adjustments", - ], -}) - -// retrieve line item adjustments -const lineItemAdjustments: ComputeActionItemLine[] = [] -cart.items.forEach((item) => { - const filteredAdjustments = item.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - lineItemAdjustments.push({ - ...item, - adjustments: filteredAdjustments, - }) - } -}) - -// retrieve shipping method adjustments -const shippingMethodAdjustments: ComputeActionShippingLine[] = - [] -cart.shipping_methods.forEach((shippingMethod) => { - const filteredAdjustments = - shippingMethod.adjustments?.filter( - (adjustment) => adjustment.code !== undefined - ) as unknown as ComputeActionAdjustmentLine[] - if (filteredAdjustments.length) { - shippingMethodAdjustments.push({ - ...shippingMethod, - adjustments: filteredAdjustments, - }) - } -}) - -// compute actions -const actions = await promotionModuleService.computeActions( - ["promo_123"], - { - items: lineItemAdjustments, - shipping_methods: shippingMethodAdjustments, - } -) -``` - -The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. - -Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. - -```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - AddItemAdjustmentAction, - AddShippingMethodAdjustment, - // ... -} from "@medusajs/framework/types" - -// ... - -await cartModuleService.setLineItemAdjustments( - cart.id, - actions.filter( - (action) => action.action === "addItemAdjustment" - ) as AddItemAdjustmentAction[] -) - -await cartModuleService.setShippingMethodAdjustments( - cart.id, - actions.filter( - (action) => - action.action === "addShippingMethodAdjustment" - ) as AddShippingMethodAdjustment[] -) -``` - - -# Authentication Flows with the Auth Main Service - -In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. - -## Authentication Methods - -### Register - -The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. - -For example: - -```ts -const data = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` - -This method calls the `register` method of the provider specified in the first parameter and returns its data. - -### Authenticate - -To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: - -```ts -const data = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) -``` - -This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. - -*** - -## Auth Flow 1: Basic Authentication - -The basic authentication flow requires first using the `register` method, then the `authenticate` method: - -```ts -const { success, authIdentity, error } = await authModuleService.register( - "emailpass", - // passed to auth provider - { - // ... - } -) - -if (error) { - // registration failed - // TODO return an error - return -} - -// later (can be another route for log-in) -const { success, authIdentity, location } = await authModuleService.authenticate( - "emailpass", - // passed to auth provider - { - // ... - } -) - -if (success && !location) { - // user is authenticated -} -``` - -If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. - -The next section explains the flow if `location` is set. - -Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. - -![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) - -### Auth Identity with Same Identifier - -If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. - -There are two ways to handle this: - -- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. -- Return an error message to the customer, informing them that the email is already in use. - -*** - -## Auth Flow 2: Third-Party Service Authentication - -The third-party service authentication method requires using the `authenticate` method first: - -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - } -) - -if (location) { - // return the location for the front-end to redirect to -} - -if (!success) { - // authentication failed -} - -// authentication successful -``` - -If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. - -For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. - -![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) - -### Overriding Callback URL - -The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. - -```ts -const { success, authIdentity, location } = await authModuleService.authenticate( - "google", - // passed to auth provider - { - // ... - callback_url: "example.com", - } -) -``` - -### validateCallback - -Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. - -So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). - -The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: - -```ts -const { success, authIdentity } = await authModuleService.validateCallback( - "google", - // passed to auth provider - { - // request data, such as - url, - headers, - query, - body, - protocol, - } -) - -if (success) { - // authentication succeeded -} -``` - -For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. - -If the returned `success` property is `true`, the authentication with the third-party provider was successful. - -![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) - -*** - -## Reset Password - -To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. - -For example: - -```ts -const { success } = await authModuleService.updateProvider( - "emailpass", - // passed to the auth provider - { - entity_id: "user@example.com", - password: "supersecret", - } -) - -if (success) { - // password reset successfully -} -``` - -The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. - -In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. - -If the returned `success` property is `true`, the password has reset successfully. - - -# Auth Identity and Actor Types - -In this document, you’ll learn about concepts related to identity and actors in the Auth Module. - -## What is an Auth Identity? - -The [AuthIdentity data model](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) represents a user registered by an [authentication provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). When a user is registered using an authentication provider, the provider creates a record of `AuthIdentity`. - -Then, when the user logs-in in the future with the same authentication provider, the associated auth identity is used to validate their credentials. - -*** - -## Actor Types - -An actor type is a type of user that can be authenticated. The Auth Module doesn't store or manage any user-like models, such as for customers or users. Instead, the user types are created and managed by other modules. For example, a customer is managed by the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md). - -Then, when an auth identity is created for the actor type, the ID of the user is stored in the `app_metadata` property of the auth identity. - -For example, an auth identity of a customer has the following `app_metadata` property: - -```json -{ - "app_metadata": { - "customer_id": "cus_123" - } -} -``` - -The ID of the user is stored in the key `{actor_type}_id` of the `app_metadata` property. - -*** - -## Protect Routes by Actor Type - -When you protect routes with the `authenticate` middleware, you specify in its first parameter the actor type that must be authenticated to access the specified API routes. - -For example: - -```ts title="src/api/middlewares.ts" highlights={highlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/custom/admin*", - middlewares: [ - authenticate("user", ["session", "bearer", "api-key"]), - ], - }, - ], -}) -``` - -By specifying `user` as the first parameter of `authenticate`, only authenticated users of actor type `user` (admin users) can access API routes starting with `/custom/admin`. - -*** - -## Custom Actor Types - -You can define custom actor types that allows a custom user, managed by your custom module, to authenticate into Medusa. - -For example, if you have a custom module with a `Manager` data model, you can authenticate managers with the `manager` actor type. - -Learn how to create a custom actor type in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/create-actor-type/index.html.md). - - -# How to Use Authentication Routes - -In this document, you'll learn about the authentication routes and how to use them to create and log-in users, and reset their password. - -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" -} -``` - - -# 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. - - -# How to Create an Actor Type - -In this document, learn how to create an actor type and authenticate its associated data model. - -## 0. Create Module with Data Model - -Before creating an actor type, you must have a module with a data model representing the actor type. - -Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - -The rest of this guide uses this `Manager` data model as an example: - -```ts title="src/modules/manager/models/manager.ts" -import { model } from "@medusajs/framework/utils" - -const Manager = model.define("manager", { - id: model.id().primaryKey(), - firstName: model.text(), - lastName: model.text(), - email: model.text(), -}) - -export default Manager -``` - -*** - -## 1. Create Workflow - -Start by creating a workflow that does two things: - -- Creates a record of the `Manager` data model. -- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. - -For example, create the file `src/workflows/create-manager.ts`. with the following content: - -```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} -import { - createWorkflow, - createStep, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, -} from "@medusajs/medusa/core-flows" -import ManagerModuleService from "../modules/manager/service" - -type CreateManagerWorkflowInput = { - manager: { - first_name: string - last_name: string - email: string - } - authIdentityId: string -} - -const createManagerStep = createStep( - "create-manager-step", - async ({ - manager: managerData, - }: Pick, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") - - const manager = await managerModuleService.createManager( - managerData - ) - - return new StepResponse(manager) - } -) - -const createManagerWorkflow = createWorkflow( - "create-manager", - function (input: CreateManagerWorkflowInput) { - const manager = createManagerStep({ - manager: input.manager, - }) - - setAuthAppMetadataStep({ - authIdentityId: input.authIdentityId, - actorType: "manager", - value: manager.id, - }) - - return new WorkflowResponse(manager) - } -) - -export default createManagerWorkflow -``` - -This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. - -The workflow has two steps: - -1. Create the manager using the `createManagerStep`. -2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. - -*** - -## 2. Define the Create API Route - -Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. - -So, create the file `src/api/manager/route.ts` with the following content: - -```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} -import type { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { MedusaError } from "@medusajs/framework/utils" -import createManagerWorkflow from "../../workflows/create-manager" - -type RequestBody = { - first_name: string - last_name: string - email: string -} - -export async function POST( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) { - // If `actor_id` is present, the request carries - // authentication for an existing manager - if (req.auth_context.actor_id) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Request already authenticated as a manager." - ) - } - - const { result } = await createManagerWorkflow(req.scope) - .run({ - input: { - manager: req.body, - authIdentityId: req.auth_context.auth_identity_id, - }, - }) - - res.status(200).json({ manager: result }) -} -``` - -Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: - -1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). -2. Passing the token in the bearer header of the request to this route. - -In the API route, you create the manager using the workflow from the previous section and return it in the response. - -*** - -## 3. Apply the `authenticate` Middleware - -The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. - -To do that, create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} -import { - defineMiddlewares, - authenticate, -} from "@medusajs/framework/http" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/manager", - method: "POST", - middlewares: [ - authenticate("manager", ["session", "bearer"], { - allowUnregistered: true, - }), - ], - }, - { - matcher: "/manager/me*", - middlewares: [ - authenticate("manager", ["session", "bearer"]), - ], - }, - ], -}) -``` - -This applies middlewares on two route patterns: - -1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. -2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. - -### Retrieve Manager API Route - -For example, create the file `src/api/manager/me/route.ts` with the following content: - -```ts title="src/api/manager/me/route.ts" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import ManagerModuleService from "../../../modules/manager/service" - -export async function GET( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -): Promise { - const query = req.scope.resolve("query") - const managerId = req.auth_context?.actor_id - - const { data: [manager] } = await query.graph({ - entity: "manager", - fields: ["*"], - filters: { - id: managerId, - }, - }, { - throwIfKeyNotFound: true, - }) - - res.json({ manager }) -} -``` - -This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. - -*** - -## Test Custom Actor Type Authentication Flow - -To authenticate managers: - -1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: - -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` - -Copy the returned token to use it in the next request. - -2. Send a `POST` request to `/manager` to create a manager: - -```bash -curl -X POST 'http://localhost:9000/manager' \ --H 'Content-Type: application/json' \ --H 'Authorization: Bearer {token}' \ ---data-raw '{ - "first_name": "John", - "last_name": "Doe", - "email": "manager@gmail.com" -}' -``` - -Replace `{token}` with the token returned in the previous step. - -3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: - -```bash -curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ --H 'Content-Type: application/json' \ ---data-raw '{ - "email": "manager@gmail.com", - "password": "supersecret" -}' -``` - -4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: - -```bash -curl 'http://localhost:9000/manager/me' \ --H 'Authorization: Bearer {token}' -``` - -Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. - -*** - -## Delete User of Actor Type - -When you delete a user of the actor type, you must update its auth identity to remove the association to the user. - -For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: - -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import ManagerModuleService from "../modules/manager/service" - -export type DeleteManagerWorkflow = { - id: string -} - -const deleteManagerStep = createStep( - "delete-manager-step", - async ( - { id }: DeleteManagerWorkflow, - { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") - - const manager = await managerModuleService.retrieve(id) - - await managerModuleService.deleteManagers(id) - - return new StepResponse(undefined, { manager }) - }, - async ({ manager }, { container }) => { - const managerModuleService: ManagerModuleService = - container.resolve("manager") - - await managerModuleService.createManagers(manager) - } - ) -``` - -You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. - -Next, in the same file, add the workflow that deletes a manager: - -```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} -// other imports -import { MedusaError } from "@medusajs/framework/utils" -import { - WorkflowData, - WorkflowResponse, - createWorkflow, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - setAuthAppMetadataStep, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" - -// ... - -export const deleteManagerWorkflow = createWorkflow( - "delete-manager", - ( - input: WorkflowData - ): WorkflowResponse => { - deleteManagerStep(input) - - const { data: authIdentities } = useQueryGraphStep({ - entity: "auth_identity", - fields: ["id"], - filters: { - app_metadata: { - // the ID is of the format `{actor_type}_id`. - manager_id: input.id, - }, - }, - }) - - const authIdentity = transform( - { authIdentities }, - ({ authIdentities }) => { - const authIdentity = authIdentities[0] - - if (!authIdentity) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - "Auth identity not found" - ) - } - - return authIdentity - } - ) - - setAuthAppMetadataStep({ - authIdentityId: authIdentity.id, - actorType: "manager", - value: null, - }) - - return new WorkflowResponse(input.id) - } -) -``` - -In the workflow, you: - -1. Use the `deleteManagerStep` defined earlier to delete the manager. -2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. -3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. - -You can use this workflow when deleting a manager, such as in an API route. - - -# Auth Module Options - -In this document, you'll learn about the options of the Auth Module. - -## providers - -The `providers` option is an array of auth module providers. - -When the Medusa application starts, these providers are registered and can be used to handle authentication. - -By default, the `emailpass` provider is registered to authenticate customers and admin users. - -For example: - -```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: [ - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // provider 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. - -*** - -## Auth CORS - -The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. - -By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. - -Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. - -*** - -## authMethodsPerActor Configuration - -The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. - -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). - - -# 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). - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. - -You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. - -### Prerequisites - -- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) - -## 1. Create Subscriber - -The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. - -Create the file `src/subscribers/handle-reset.ts` with the following content: - -```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" -import { - SubscriberArgs, - type SubscriberConfig, -} from "@medusajs/medusa" -import { Modules } from "@medusajs/framework/utils" - -export default async function resetPasswordTokenHandler({ - event: { data: { - entity_id: email, - token, - actor_type, - } }, - container, -}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) - - const urlPrefix = actor_type === "customer" ? - "https://storefront.com" : - "https://admin.com/app" - - await notificationModuleService.createNotifications({ - to: email, - channel: "email", - template: "reset-password-template", - data: { - // a URL to a frontend application - url: `${urlPrefix}/reset-password?token=${token}&email=${email}`, - }, - }) -} - -export const config: SubscriberConfig = { - event: "auth.password_reset", -} -``` - -You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: - -- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. -- `token`: The token to reset the user's password. -- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. - -This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). - -In the subscriber, you: - -- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. -- Resolve the Notification Module and use its `createNotifications` method to send the notification. -- You pass to the `createNotifications` method an object having the following properties: - - `to`: The identifier to send the notification to, which in this case is the email. - - `channel`: The channel to send the notification through, which in this case is email. - - `template`: The template ID in the third-party service. - - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. - -*** - -## 2. Test it Out: Generate Reset Password Token - -To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. - -For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: - -```bash -curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ ---header 'Content-Type: application/json' \ ---data-raw '{ - "identifier": "admin-test@gmail.com" -}' -``` - -In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. - -If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: - -```plain -info: Processing auth.password_reset which has 1 subscribers -``` - -The notification is sent to the user with the frontend URL to enter a new password. - -*** - -## Next Steps: Implementing Frontend - -In your frontend, you must have a page that accepts `token` and `email` query parameters. - -The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). - -### Examples - -- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) - - # Fulfillment Concepts In this document, you’ll learn about some basic fulfillment concepts. @@ -24396,51 +22445,6 @@ createRemoteLinkStep({ ``` -# 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. @@ -24509,108 +22513,49 @@ When fulfilling an item, you might use a third-party fulfillment provider that r The `ShippingOption` data model has a `data` property. It's an object that stores custom data relevant later when creating and processing a fulfillment. -# Inventory Concepts +# Fulfillment Module Options -In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. +In this document, you'll learn about the options of the Fulfillment Module. -## InventoryItem +## providers -An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. +The `providers` option is an array of fulfillment module providers. -The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. +When the Medusa application starts, these providers are registered and can be used to process fulfillments. -![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) +For example: -### Inventory Shipping Requirement +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" -An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. +// ... -When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). +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: -## InventoryLevel - -An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. - -It has three quantity-related properties: - -- `stocked_quantity`: The available stock quantity of an item in the associated location. -- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. -- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. - -### Associated Location - -The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. - -*** - -## ReservationItem - -A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. - -The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. - - -# Inventory Module in Medusa Flows - -This document explains how the Inventory Module is used within the Medusa application's flows. - -## Product Variant Creation - -When a product variant is created and its `manage_inventory` property's value is `true`, the Medusa application creates an inventory item associated with that product variant. - -This flow is implemented within the [createProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductVariantsWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the product variant creation form](https://res.cloudinary.com/dza7lstvk/image/upload/v1709661511/Medusa%20Resources/inventory-product-create_khz2hk.jpg) - -*** - -## Add to Cart - -When a product variant with `manage_inventory` set to `true` is added to cart, the Medusa application checks whether there's sufficient stocked quantity. If not, an error is thrown and the product variant won't be added to the cart. - -This flow is implemented within the [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the add to cart flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709711645/Medusa%20Resources/inventory-cart-flow_achwq9.jpg) - -*** - -## Order Placed - -When an order is placed, the Medusa application creates a reservation item for each product variant with `manage_inventory` set to `true`. - -This flow is implemented within the [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order placed flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712005/Medusa%20Resources/inventory-order-placed_qdxqdn.jpg) - -*** - -## Order Fulfillment - -When an item in an order is fulfilled and the associated variant has its `manage_inventory` property set to `true`, the Medusa application: - -- Subtracts the `reserved_quantity` from the `stocked_quantity` in the inventory level associated with the variant's inventory item. -- Resets the `reserved_quantity` to `0`. -- Deletes the associated reservation item. - -This flow is implemented within the [createOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderFulfillmentWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order fulfillment flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712390/Medusa%20Resources/inventory-order-fulfillment_o9wdxh.jpg) - -*** - -## Order Return - -When an item in an order is returned and the associated variant has its `manage_inventory` property set to `true`, the Medusa application increments the `stocked_quantity` of the inventory item's level with the returned quantity. - -This flow is implemented within the [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) - -![A diagram showcasing how the Inventory Module is used in the order return flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1709712457/Medusa%20Resources/inventory-order-return_ihftyk.jpg) - -### Dismissed Returned Items - -If a returned item is considered damaged or is dismissed, its quantity doesn't increment the `stocked_quantity` of the inventory item's level. +- `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. # Inventory Kits @@ -25003,6 +22948,110 @@ 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. + + +# Inventory Concepts + +In this document, you’ll learn about the main concepts in the Inventory Module, and how data is stored and related. + +## InventoryItem + +An inventory item, represented by the [InventoryItem data model](https://docs.medusajs.com/references/inventory-next/models/InventoryItem/index.html.md), is a stock-kept item, such as a product, whose inventory can be managed. + +The `InventoryItem` data model mainly holds details related to the underlying stock item, but has relations to other data models that include its inventory details. + +![A diagram showcasing the relation between data models in the Inventory Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1709658103/Medusa%20Resources/inventory-architecture_kxr2ql.png) + +### Inventory Shipping Requirement + +An inventory item has a `requires_shipping` field (enabled by default) that indicates whether the item requires shipping. For example, if you're selling a digital license that has limited stock quantity but doesn't require shipping. + +When a product variant is purchased in the Medusa application, this field is used to determine whether the item requires shipping. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md). + +*** + +## InventoryLevel + +An inventory level, represented by the [InventoryLevel data model](https://docs.medusajs.com/references/inventory-next/models/InventoryLevel/index.html.md), holds the inventory and quantity details of an inventory item in a specific location. + +It has three quantity-related properties: + +- `stocked_quantity`: The available stock quantity of an item in the associated location. +- `reserved_quantity`: The quantity reserved from the available `stocked_quantity`. It indicates the quantity that's still not removed from stock, but considered as unavailable when checking whether an item is in stock. +- `incoming_quantity`: The incoming stock quantity of an item into the associated location. This property doesn't play into the `stocked_quantity` or when checking whether an item is in stock. + +### Associated Location + +The inventory level's location is determined by the `location_id` property. Medusa links the `InventoryLevel` data model with the `StockLocation` data model from the Stock Location Module. + +*** + +## ReservationItem + +A reservation item, represented by the [ReservationItem](https://docs.medusajs.com/references/inventory-next/models/ReservationItem/index.html.md) data model, represents unavailable quantity of an inventory item in a location. It's used when an order is placed but not fulfilled yet. + +The reserved quantity is associated with a location, so it has a similar relation to that of the `InventoryLevel` with the Stock Location Module. + + # Links between Inventory Module and Other Modules This document showcases the module links defined between the Inventory Module and other Commerce Modules. @@ -25144,60 +23193,6 @@ const { data: inventoryLevels } = useQueryGraphStep({ ``` -# 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 @@ -25304,6 +23299,60 @@ 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 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 Exchange In this document, you’ll learn about order exchanges. @@ -25911,106 +23960,6 @@ When the order is changed, such as an item is exchanged, this changes the versio When the order is retrieved, only the related data having the same version is retrieved. -# Order Return - -In this document, you’ll learn about order returns. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. - -## What is a Return? - -A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). - -A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. - -![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) - -Once the merchant receives the returned items, they mark the return as received. - -*** - -## Returned Items - -The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). - -The `ReturnItem` model has two properties storing the item's quantity: - -1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. -2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. - -*** - -## Return Shipping Methods - -A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). - -In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. - -*** - -## Refund Payment - -The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. - -The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. - -*** - -## Returns in Exchanges and Claims - -When a merchant creates an exchange or a claim, it includes returning items from the customer. - -The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. - -*** - -## How Returns Impact an Order’s Version - -The order’s version is incremented when: - -1. A return is requested. -2. A return is marked as received. - - -# Order Change - -In this document, you'll learn about the Order Change data model and possible actions in it. - -## OrderChange Data Model - -The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. - -Its `change_type` property indicates what the order change is created for: - -1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). -2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). -3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). -4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). - -Once the order change is confirmed, its changes are applied on the order. - -*** - -## Order Change Actions - -The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). - -The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. - -The following table lists the possible `action` values that Medusa uses and what `details` they carry. - -|Action|Description|Details| -|---|---|---|---|---| -|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| -|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| -|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| -|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| -|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | -|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| - - # Promotions Adjustments in Orders In this document, you’ll learn how a promotion is applied to an order’s items and shipping methods using adjustment lines. @@ -26133,6 +24082,67 @@ await orderModuleService.setOrderShippingMethodAdjustments( ``` +# Order Return + +In this document, you’ll learn about order returns. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/orders/returns/index.html.md) to learn how to manage an order's returns using the dashboard. + +## What is a Return? + +A return is the return of items delivered from the customer back to the merchant. It is represented by the [Return data model](https://docs.medusajs.com/references/order/models/Return/index.html.md). + +A return is requested either by the customer from the storefront, or the merchant from the admin. Medusa supports an automated Return Merchandise Authorization (RMA) flow. + +![Diagram showcasing the automated RMA flow.](https://res.cloudinary.com/dza7lstvk/image/upload/v1719578128/Medusa%20Resources/return-rma_pzprwq.jpg) + +Once the merchant receives the returned items, they mark the return as received. + +*** + +## Returned Items + +The items to be returned are represented by the [ReturnItem data model](references/order/models/ReturnItem). + +The `ReturnItem` model has two properties storing the item's quantity: + +1. `received_quantity`: The quantity of the item that's received and can be added to the item's inventory quantity. +2. `damaged_quantity`: The quantity of the item that's damaged, meaning it can't be sold again or added to the item's inventory quantity. + +*** + +## Return Shipping Methods + +A return has shipping methods used to return the items to the merchant. The shipping methods are represented by the [OrderShippingMethod data model](references/order/models/OrderShippingMethod). + +In the Medusa application, the shipping method for a return is created only from a shipping option, provided by the Fulfillment Module, that has the rule `is_return` enabled. + +*** + +## Refund Payment + +The `refund_amount` property of the `Return` data model holds the amount a merchant must refund the customer. + +The [OrderTransaction data model](references/order/models/OrderTransaction) represents the refunds made for the return. + +*** + +## Returns in Exchanges and Claims + +When a merchant creates an exchange or a claim, it includes returning items from the customer. + +The `Return` data model also represents the return of these items. In this case, the return is associated with the exchange or claim it was created for. + +*** + +## How Returns Impact an Order’s Version + +The order’s version is incremented when: + +1. A return is requested. +2. A return is marked as received. + + # Tax Lines in Order Module In this document, you’ll learn about tax lines in an order. @@ -26210,6 +24220,1007 @@ The `OrderTransaction` data model has two properties that determine which data m - `reference_id`: indicates the ID of the record in the table. For example, `pay_123`. +# 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 +``` + + +# Order Change + +In this document, you'll learn about the Order Change data model and possible actions in it. + +## OrderChange Data Model + +The [OrderChange data model](https://docs.medusajs.com/references/order/models/OrderChange/index.html.md) represents any kind of change to an order, such as a return, exchange, or edit. + +Its `change_type` property indicates what the order change is created for: + +1. `edit`: The order change is making edits to the order, as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/edit/index.html.md). +2. `exchange`: The order change is associated with an exchange, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/exchange/index.html.md). +3. `claim`: The order change is associated with a claim, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/claim/index.html.md). +4. `return_request` or `return_receive`: The order change is associated with a return, which you can learn about in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/return/index.html.md). + +Once the order change is confirmed, its changes are applied on the order. + +*** + +## Order Change Actions + +The actions to perform on the original order by a change, such as adding an item, are represented by the [OrderChangeAction data model](https://docs.medusajs.com/references/order/models/OrderChangeAction/index.html.md). + +The `OrderChangeAction` has an `action` property that indicates the type of action to perform on the order, and a `details` property that holds more details related to the action. + +The following table lists the possible `action` values that Medusa uses and what `details` they carry. + +|Action|Description|Details| +|---|---|---|---|---| +|\`ITEM\_ADD\`|Add an item to the order.|\`details\`| +|\`ITEM\_UPDATE\`|Update an item in the order.|\`details\`| +|\`RETURN\_ITEM\`|Set an item to be returned.|\`details\`| +|\`RECEIVE\_RETURN\_ITEM\`|Mark a return item as received.|\`details\`| +|\`RECEIVE\_DAMAGED\_RETURN\_ITEM\`|Mark a return item that's damaged as received.|\`details\`| +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`SHIPPING\_ADD\`|Add a shipping method for new or returned items.|No details added. The ID to the shipping method is added in the | +|\`WRITE\_OFF\_ITEM\`|Remove an item's quantity as part of the claim, without adding the quantity back to the item variant's inventory.|\`details\`| + + +# 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. + + # Pricing Concepts In this document, you’ll learn about the main concepts in the Pricing Module. @@ -26233,6 +25244,54 @@ A price list has optional `start_date` and `end_date` properties that indicate t Its associated prices are represented by the `Price` data model. +# 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 Pricing Module and Other Modules This document showcases the module links defined between the Pricing Module and other Commerce Modules. @@ -26643,55 +25702,6 @@ const price = await pricingModuleService.calculatePrices( ### Result -# 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). - - # Tax-Inclusive Pricing In this document, you’ll learn about tax-inclusive pricing and how it's used when calculating prices. @@ -27029,48 +26039,161 @@ 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. -# Links between Payment Module and Other Modules +# Configure Selling Products -This document showcases the module links defined between the Payment Module and other Commerce Modules. +In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. + +The concepts in this guide are applicable starting from Medusa v2.5.1. + +## Scenario + +Businesses can have different selling requirements: + +1. They may sell physical or digital items. +2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. +3. They may sell items whose inventory is managed by an external system, such as an ERP. + +Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. + +*** + +## Configuring Shipping Requirements + +The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. + +When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. + +If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. + +### Overriding Shipping Requirements for Variants + +A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. + +Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). + +When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: + +1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. +2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. + +*** + +## Use Case Examples + +By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: + +|Use Case|Configurations|Example| +|---|---|---|---|---| +|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| +|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| +|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| +|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| + + +# Product Variant Inventory + +# Product Variant Inventory + +In this guide, you'll learn about the inventory management features related to product variants. + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants. + +## Configure Inventory Management of Product Variants + +A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP. + +The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. + +When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. + +*** + +## How the Medusa Application Manages Inventory + +When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. + +![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) + +The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. + +![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) + +Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). + +The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. + +![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) + +Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). + +### Product Inventory in Storefronts + +When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. + +The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. + +For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. + +![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) + +*** + +## Variant Back Orders + +Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. + +You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). + +*** + +## Additional Resources + +The following guides provide more details on inventory management in the Medusa application: + +- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. +- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant. +- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products. +- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. +- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). + + +# Links between Region Module and Other Modules + +This document showcases the module links defined between the Region Module and other Commerce Modules. ## Summary -The Payment Module has the following links to other modules: +The Region Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |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| +|Cart|Region|Read-only - has one|Learn more| +|Order|Region|Read-only - has one|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). +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 `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. ### 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`: +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: ### query.graph ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "cart.*", + "region.*", ], }) -// paymentCollections[0].cart +// carts[0].region ``` ### useQueryGraphStep @@ -27080,159 +26203,37 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "cart.*", + "region.*", ], }) -// 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", - }, -}) +// carts[0].region ``` *** ## 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) +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 `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. ### 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`: +To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: ### query.graph ```ts -const { data: paymentCollections } = await query.graph({ - entity: "payment_collection", +const { data: orders } = await query.graph({ + entity: "order", fields: [ - "order.*", + "region.*", ], }) -// paymentCollections[0].order +// orders[0].region ``` ### useQueryGraphStep @@ -27242,80 +26243,41 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentCollections } = useQueryGraphStep({ - entity: "payment_collection", +const { data: orders } = useQueryGraphStep({ + entity: "order", fields: [ - "order.*", + "region.*", ], }) -// 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", - }, -}) +// orders[0].region ``` *** -## Region Module +## Payment 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. +You can specify for each region which payment providers are available for use. + +Medusa defines a module 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`: +To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: ### query.graph ```ts -const { data: paymentProviders } = await query.graph({ - entity: "payment_provider", +const { data: regions } = await query.graph({ + entity: "region", fields: [ - "regions.*", + "payment_providers.*", ], }) -// paymentProviders[0].regions +// regions[0].payment_providers ``` ### useQueryGraphStep @@ -27325,14 +26287,14 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: paymentProviders } = useQueryGraphStep({ - entity: "payment_provider", +const { data: regions } = useQueryGraphStep({ + entity: "region", fields: [ - "regions.*", + "payment_providers.*", ], }) -// paymentProviders[0].regions +// regions[0].payment_providers ``` ### Manage with Link @@ -27375,564 +26337,6 @@ createRemoteLinkStep({ ``` -# Payment Module Options - -In this document, you'll learn about the options of the Payment Module. - -## All Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`webhook\_delay\`|A number indicating the delay in milliseconds before processing a webhook event.|No|\`5000\`| -|\`webhook\_retries\`|The number of times to retry the webhook event processing in case of an error.|No|\`3\`| -|\`providers\`|An array of payment providers to install and register. Learn more |No|-| - -*** - -## providers Option - -The `providers` option is an array of payment module providers. - -When the Medusa application starts, these providers are registered and can be used to process payments. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/payment", - options: { - providers: [ - { - resolve: "@medusajs/medusa/payment-stripe", - id: "stripe", - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -The `providers` option is an array of objects that accept the following properties: - -- `resolve`: A string indicating the package name of the module provider or the path to it relative to the `src` directory. -- `id`: A string indicating the provider's unique name or ID. -- `options`: An optional object of the module provider's options. - - -# Payment - -In this document, you’ll learn what a payment is and how it's created, captured, and refunded. - -## 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 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 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 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. - - # Promotion Actions In this document, you’ll learn about promotion actions and how they’re computed using the [computeActions method](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). @@ -28269,189 +26673,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`. -# Links between Promotion Module and Other Modules - -This document showcases the module links defined between the Promotion Module and other Commerce Modules. - -## Summary - -The Promotion Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|Promotion|Stored - many-to-many|Learn more| -|LineItemAdjustment|Promotion|Read-only - has one|Learn more| -|Order|Promotion|Stored - many-to-many|Learn more| - -*** - -## Cart Module - -A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. - -![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) - -Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. - -### Retrieve with Query - -To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: - -To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions[0].carts -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "carts.*", - ], -}) - -// promotions[0].carts -``` - -### Manage with Link - -To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.CART]: { - cart_id: "cart_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -*** - -## Order Module - -An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. - -![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) - -### Retrieve with Query - -To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: - -### query.graph - -```ts -const { data: promotions } = await query.graph({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions[0].orders -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: promotions } = useQueryGraphStep({ - entity: "promotion", - fields: [ - "orders.*", - ], -}) - -// promotions[0].orders -``` - -### Manage with Link - -To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): - -### link.create - -```ts -import { Modules } from "@medusajs/framework/utils" - -// ... - -await link.create({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - -### createRemoteLinkStep - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" - -// ... - -createRemoteLinkStep({ - [Modules.ORDER]: { - order_id: "order_123", - }, - [Modules.PROMOTION]: { - promotion_id: "promo_123", - }, -}) -``` - - # Links between Product Module and Other Modules This document showcases the module links defined between the Product Module and other Commerce Modules. @@ -28898,304 +27119,6 @@ createRemoteLinkStep({ ``` -# Configure Selling Products - -In this guide, you'll learn how to set up and configure your products based on their shipping and inventory requirements, the product type, how you want to sell them, or your commerce ecosystem. - -The concepts in this guide are applicable starting from Medusa v2.5.1. - -## Scenario - -Businesses can have different selling requirements: - -1. They may sell physical or digital items. -2. They may sell items that don't require shipping or inventory management, such as selling digital products, services, or booking appointments. -3. They may sell items whose inventory is managed by an external system, such as an ERP. - -Medusa supports these different selling requirements by allowing you to configure shipping and inventory requirements for products and their variants. This guide explains how these configurations work, then provides examples of setting up different use cases. - -*** - -## Configuring Shipping Requirements - -The Medusa application defines a link between the `Product` data model and a [ShippingProfile](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/concepts#shipping-profile/index.html.md) in the [Fulfillment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/fulfillment/index.html.md), allowing you to associate a product with a shipping profile. - -When a product is associated with a shipping profile, its variants require shipping and fulfillment when purchased. This is useful for physical products or digital products that require custom fulfillment. - -If a product doesn't have an associated shipping profile, its variants don't require shipping and fulfillment when purchased. This is useful for digital products, for example, that don't require shipping. - -### Overriding Shipping Requirements for Variants - -A product variant whose inventory is managed by Medusa (its `manage_inventory` property is enabled) has an [inventory item](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts#inventoryitem/index.html.md). The inventory item has a `requires_shipping` property that can be used to override its shipping requirement. This is useful if the product has an associated shipping profile but you want to disable shipping for a specific variant, or vice versa. - -Learn more about product variant's inventory in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/variant-inventory/index.html.md). - -When a product variant is purchased, the Medusa application decides whether the purchased item requires shipping in the following order: - -1. The product variant has an inventory item. In this case, the Medusa application uses the inventory item's `requires_shipping` property to determine if the item requires shipping. -2. If the product variant doesn't have an inventory item, the Medusa application checks whether the product has an associated shipping profile to determine if the item requires shipping. - -*** - -## Use Case Examples - -By combining configurations of shipment requirements and inventory management, you can set up your products to support your use case: - -|Use Case|Configurations|Example| -|---|---|---|---|---| -|Item that's shipped on purchase, and its variant inventory is managed by the Medusa application.||Any stock-kept item (clothing, for example), whose inventory is managed in the Medusa application.| -|Item that's shipped on purchase, but its variant inventory is managed externally (not by Medusa) or it has infinite stock.||Any stock-kept item (clothing, for example), whose inventory is managed in an ERP or has infinite stock.| -|Item that's not shipped on purchase, but its variant inventory is managed by Medusa.||Digital products, such as licenses, that don't require shipping but have a limited quantity.| -|Item that doesn't require shipping and its variant inventory isn't managed by Medusa.||| - - -# Product Variant Inventory - -# Product Variant Inventory - -In this guide, you'll learn about the inventory management features related to product variants. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/products/variants#manage-product-variant-inventory/index.html.md) to learn how to manage inventory of product variants. - -## Configure Inventory Management of Product Variants - -A product variant, represented by the [ProductVariant](https://docs.medusajs.com/references/product/models/ProductVariant/index.html.md) data model, has a `manage_inventory` field that's disabled by default. This field indicates whether you'll manage the inventory quantity of the product variant in the Medusa application. You can also keep `manage_inventory` disabled if you manage the product's inventory in an external system, such as an ERP. - -The Product Module doesn't provide inventory-management features. Instead, the Medusa application uses the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) to manage inventory for products and variants. When `manage_inventory` is disabled, the Medusa application always considers the product variant to be in stock. This is useful if your product's variants aren't items that can be stocked, such as digital products, or they don't have a limited stock quantity. - -When `manage_inventory` is enabled, the Medusa application tracks the inventory of the product variant using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md). For example, when a customer purchases a product variant, the Medusa application decrements the stocked quantity of the product variant. - -*** - -## How the Medusa Application Manages Inventory - -When a product variant has `manage_inventory` enabled, the Medusa application creates an inventory item using the [Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/index.html.md) and links it to the product variant. - -![Diagram showcasing the link between a product variant and its inventory item](https://res.cloudinary.com/dza7lstvk/image/upload/v1709652779/Medusa%20Resources/product-inventory_kmjnud.jpg) - -The inventory item has one or more locations, called inventory levels, that represent the stock quantity of the product variant at a specific location. This allows you to manage inventory across multiple warehouses, such as a warehouse in the US and another in Europe. - -![Diagram showcasing the link between a variant and its inventory item, and the inventory item's level.](https://res.cloudinary.com/dza7lstvk/image/upload/v1738580390/Medusa%20Resources/variant-inventory-level_bbee2t.jpg) - -Learn more about inventory concepts in the [Inventory Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/concepts/index.html.md). - -The Medusa application represents and manages stock locations using the [Stock Location Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/index.html.md). It creates a read-only link between the `InventoryLevel` and `StockLocation` data models so that it can retrieve the stock location of an inventory level. - -![Diagram showcasing the read-only link between an inventory level and a stock location](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-level-stock_amxfg5.jpg) - -Learn more about the Stock Location Module in the [Stock Location Module's documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/stock-location/concepts/index.html.md). - -### Product Inventory in Storefronts - -When a storefront sends a request to the Medusa application, it must always pass a [publishable API key](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/publishable-api-keys/index.html.md) in the request header. This API key specifies the sales channels, available through the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md), of the storefront. - -The Medusa application links sales channels to stock locations, indicating the locations available for a specific sales channel. So, all inventory-related operations are scoped by the sales channel and its associated stock locations. - -For example, the availability of a product variant is determined by the `stocked_quantity` of its inventory level at the stock location linked to the storefront's sales channel. - -![Diagram showcasing the overall relations between inventory, stock location, and sales channel concepts](https://res.cloudinary.com/dza7lstvk/image/upload/v1738582163/Medusa%20Resources/inventory-stock-sales_fknoxw.jpg) - -*** - -## Variant Back Orders - -Product variants have an `allow_backorder` field that's disabled by default. When enabled, the Medusa application allows customers to purchase the product variant even when it's out of stock. Use this when your product variant is available through on-demand or pre-order purchase. - -You can also allow customers to subscribe to restock notifications of a product variant as explained in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/recipes/commerce-automation/restock-notification/index.html.md). - -*** - -## Additional Resources - -The following guides provide more details on inventory management in the Medusa application: - -- [Inventory Kits in the Inventory Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-kit/index.html.md): Learn how you can implement bundled or multi-part products through the Inventory Module. -- [Retrieve Product Variant Inventory Quantity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/guides/variant-inventory/index.html.md): Learn how to retrieve the available inventory quantity of a product variant. -- [Configure Selling Products](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/selling-products/index.html.md): Learn how to use inventory management to support different use cases when selling products. -- [Inventory in Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/inventory/inventory-in-flows/index.html.md): Learn how Medusa utilizes inventory management in different flows. -- [Storefront guide: how to retrieve a product variant's inventory details](https://docs.medusajs.com/resources/storefront-development/products/inventory/index.html.md). - - -# Links between Region Module and Other Modules - -This document showcases the module links defined between the Region Module and other Commerce Modules. - -## Summary - -The Region Module has the following links to other modules: - -Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. - -|First Data Model|Second Data Model|Type|Description| -|---|---|---|---| -|Cart|Region|Read-only - has one|Learn more| -|Order|Region|Read-only - has one|Learn more| -|Region|PaymentProvider|Stored - many-to-many|Learn more| - -*** - -## 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 `Region` data model. Because the link is read-only from the `Cart`'s side, you can only retrieve the region of a cart, and not the other way around. - -### Retrieve with Query - -To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: carts } = await query.graph({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "region.*", - ], -}) - -// carts[0].region -``` - -*** - -## 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 `Region` data model. Because the link is read-only from the `Order`'s side, you can only retrieve the region of an order, and not the other way around. - -### Retrieve with Query - -To retrieve the region of an order with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: - -### query.graph - -```ts -const { data: orders } = await query.graph({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "region.*", - ], -}) - -// orders[0].region -``` - -*** - -## Payment Module - -You can specify for each region which payment providers are available for use. - -Medusa defines a module 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) - -### Retrieve with Query - -To retrieve the payment providers of a region with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_providers.*` in `fields`: - -### query.graph - -```ts -const { data: regions } = await query.graph({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions[0].payment_providers -``` - -### useQueryGraphStep - -```ts -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" - -// ... - -const { data: regions } = useQueryGraphStep({ - entity: "region", - fields: [ - "payment_providers.*", - ], -}) - -// regions[0].payment_providers -``` - -### 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", - }, -}) -``` - - # Links between Sales Channel Module and Other Modules This document showcases the module links defined between the Sales Channel Module and other Commerce Modules. @@ -29599,6 +27522,1435 @@ You can then use these IDs based on your business logic. For example, you can re Notice that the request object's type is `MedusaStoreRequest` instead of `MedusaRequest` to ensure the availability of the `publishable_key_context` property. +# Authentication Flows with the Auth Main Service + +In this document, you'll learn how to use the Auth Module's main service's methods to implement authentication flows and reset a user's password. + +## Authentication Methods + +### Register + +The [register method of the Auth Module's main service](https://docs.medusajs.com/references/auth/register/index.html.md) creates an auth identity that can be authenticated later. + +For example: + +```ts +const data = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `register` method of the provider specified in the first parameter and returns its data. + +### Authenticate + +To authenticate a user, you use the [authenticate method of the Auth Module's main service](https://docs.medusajs.com/references/auth/authenticate/index.html.md). For example: + +```ts +const data = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) +``` + +This method calls the `authenticate` method of the provider specified in the first parameter and returns its data. + +*** + +## Auth Flow 1: Basic Authentication + +The basic authentication flow requires first using the `register` method, then the `authenticate` method: + +```ts +const { success, authIdentity, error } = await authModuleService.register( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (error) { + // registration failed + // TODO return an error + return +} + +// later (can be another route for log-in) +const { success, authIdentity, location } = await authModuleService.authenticate( + "emailpass", + // passed to auth provider + { + // ... + } +) + +if (success && !location) { + // user is authenticated +} +``` + +If `success` is true and `location` isn't set, the user is authenticated successfully, and their authentication details are available within the `authIdentity` object. + +The next section explains the flow if `location` is set. + +Check out the [AuthIdentity](https://docs.medusajs.com/references/auth/models/AuthIdentity/index.html.md) reference for the received properties in `authIdentity`. + +![Diagram showcasing the basic authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711373749/Medusa%20Resources/basic-auth_lgpqsj.jpg) + +### Auth Identity with Same Identifier + +If an auth identity, such as a `customer`, tries to register with an email of another auth identity, the `register` method returns an error. This can happen either if another customer is using the same email, or an admin user has the same email. + +There are two ways to handle this: + +- Consider the customer authenticated if the `authenticate` method validates that the email and password are correct. This allows admin users, for example, to authenticate as customers. +- Return an error message to the customer, informing them that the email is already in use. + +*** + +## Auth Flow 2: Third-Party Service Authentication + +The third-party service authentication method requires using the `authenticate` method first: + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + } +) + +if (location) { + // return the location for the front-end to redirect to +} + +if (!success) { + // authentication failed +} + +// authentication successful +``` + +If the `authenticate` method returns a `location` property, the authentication process requires the user to perform an action with a third-party service. So, you return the `location` to the front-end or client to redirect to that URL. + +For example, when using the `google` provider, the `location` is the URL that the user is navigated to login. + +![Diagram showcasing the first part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711374847/Medusa%20Resources/third-party-auth-1_enyedy.jpg) + +### Overriding Callback URL + +The Google and GitHub providers allow you to override their `callbackUrl` option during authentication. This is useful when you redirect the user after authentication to a URL based on its actor type. For example, you redirect admin users and customers to different pages. + +```ts +const { success, authIdentity, location } = await authModuleService.authenticate( + "google", + // passed to auth provider + { + // ... + callback_url: "example.com", + } +) +``` + +### validateCallback + +Providers handling this authentication flow must implement the `validateCallback` method. It implements the logic to validate the authentication with the third-party service. + +So, once the user performs the required action with the third-party service (for example, log-in with Google), the frontend must redirect to an API route that uses the [validateCallback method of the Auth Module's main service](https://docs.medusajs.com/references/auth/validateCallback/index.html.md). + +The method calls the specified provider’s `validateCallback` method passing it the authentication details it received in the second parameter: + +```ts +const { success, authIdentity } = await authModuleService.validateCallback( + "google", + // passed to auth provider + { + // request data, such as + url, + headers, + query, + body, + protocol, + } +) + +if (success) { + // authentication succeeded +} +``` + +For providers like Google, the `query` object contains the query parameters from the original callback URL, such as the `code` and `state` parameters. + +If the returned `success` property is `true`, the authentication with the third-party provider was successful. + +![Diagram showcasing the second part of the third-party authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1711375123/Medusa%20Resources/third-party-auth-2_kmjxju.jpg) + +*** + +## Reset Password + +To update a user's password or other authentication details, use the `updateProvider` method of the Auth Module's main service. It calls the `update` method of the specified authentication provider. + +For example: + +```ts +const { success } = await authModuleService.updateProvider( + "emailpass", + // passed to the auth provider + { + entity_id: "user@example.com", + password: "supersecret", + } +) + +if (success) { + // password reset successfully +} +``` + +The method accepts as a first parameter the ID of the provider, and as a second parameter the data necessary to reset the password. + +In the example above, you use the `emailpass` provider, so you have to pass an object having an `email` and `password` properties. + +If the returned `success` property is `true`, the password has reset successfully. + + +# Links between Promotion Module and Other Modules + +This document showcases the module links defined between the Promotion Module and other Commerce Modules. + +## Summary + +The Promotion Module has the following links to other modules: + +Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. + +|First Data Model|Second Data Model|Type|Description| +|---|---|---|---| +|Cart|Promotion|Stored - many-to-many|Learn more| +|LineItemAdjustment|Promotion|Read-only - has one|Learn more| +|Order|Promotion|Stored - many-to-many|Learn more| + +*** + +## Cart Module + +A promotion can be applied on line items and shipping methods of a cart. Medusa defines a link between the `Cart` and `Promotion` data models. + +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the [Cart Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/cart/index.html.md)'s `LineItemAdjustment` data model and the `Promotion` data model. Because the link is read-only from the `LineItemAdjustment`'s side, you can only retrieve the promotion applied on a line item, and not the other way around. + +### Retrieve with Query + +To retrieve the carts that a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `carts.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions[0].carts +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "carts.*", + ], +}) + +// promotions[0].carts +``` + +### Manage with Link + +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Order Module + +An order is associated with the promotion applied on it. Medusa defines a link between the `Order` and `Promotion` data models. + +![A diagram showcasing an example of how data models from the Order and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1716555015/Medusa%20Resources/order-promotion_dgjzzd.jpg) + +### Retrieve with Query + +To retrieve the orders a promotion is applied on with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `orders.*` in `fields`: + +### query.graph + +```ts +const { data: promotions } = await query.graph({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions[0].orders +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: promotions } = useQueryGraphStep({ + entity: "promotion", + fields: [ + "orders.*", + ], +}) + +// promotions[0].orders +``` + +### Manage with Link + +To manage the promotion of an order, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.ORDER]: { + order_id: "order_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + + +# 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 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. + + +# 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. + +## 0. Create Module with Data Model + +Before creating an actor type, you must have a module with a data model representing the actor type. + +Learn how to create a module in [this guide](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +The rest of this guide uses this `Manager` data model as an example: + +```ts title="src/modules/manager/models/manager.ts" +import { model } from "@medusajs/framework/utils" + +const Manager = model.define("manager", { + id: model.id().primaryKey(), + firstName: model.text(), + lastName: model.text(), + email: model.text(), +}) + +export default Manager +``` + +*** + +## 1. Create Workflow + +Start by creating a workflow that does two things: + +- Creates a record of the `Manager` data model. +- Sets the `app_metadata` property of the associated `AuthIdentity` record based on the new actor type. + +For example, create the file `src/workflows/create-manager.ts`. with the following content: + +```ts title="src/workflows/create-manager.ts" highlights={workflowHighlights} +import { + createWorkflow, + createStep, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, +} from "@medusajs/medusa/core-flows" +import ManagerModuleService from "../modules/manager/service" + +type CreateManagerWorkflowInput = { + manager: { + first_name: string + last_name: string + email: string + } + authIdentityId: string +} + +const createManagerStep = createStep( + "create-manager-step", + async ({ + manager: managerData, + }: Pick, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") + + const manager = await managerModuleService.createManager( + managerData + ) + + return new StepResponse(manager) + } +) + +const createManagerWorkflow = createWorkflow( + "create-manager", + function (input: CreateManagerWorkflowInput) { + const manager = createManagerStep({ + manager: input.manager, + }) + + setAuthAppMetadataStep({ + authIdentityId: input.authIdentityId, + actorType: "manager", + value: manager.id, + }) + + return new WorkflowResponse(manager) + } +) + +export default createManagerWorkflow +``` + +This workflow accepts the manager’s data and the associated auth identity’s ID as inputs. The next sections explain how the auth identity ID is retrieved. + +The workflow has two steps: + +1. Create the manager using the `createManagerStep`. +2. Set the `app_metadata` property of the associated auth identity using the `setAuthAppMetadataStep` from Medusa's core workflows. You specify the actor type `manager` in the `actorType` property of the step’s input. + +*** + +## 2. Define the Create API Route + +Next, you’ll use the workflow defined in the previous section in an API route that creates a manager. + +So, create the file `src/api/manager/route.ts` with the following content: + +```ts title="src/api/manager/route.ts" highlights={createRouteHighlights} +import type { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { MedusaError } from "@medusajs/framework/utils" +import createManagerWorkflow from "../../workflows/create-manager" + +type RequestBody = { + first_name: string + last_name: string + email: string +} + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + // If `actor_id` is present, the request carries + // authentication for an existing manager + if (req.auth_context.actor_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Request already authenticated as a manager." + ) + } + + const { result } = await createManagerWorkflow(req.scope) + .run({ + input: { + manager: req.body, + authIdentityId: req.auth_context.auth_identity_id, + }, + }) + + res.status(200).json({ manager: result }) +} +``` + +Since the manager must be associated with an `AuthIdentity` record, the request is expected to be authenticated, even if the manager isn’t created yet. This can be achieved by: + +1. Obtaining a token usng the [/auth route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). +2. Passing the token in the bearer header of the request to this route. + +In the API route, you create the manager using the workflow from the previous section and return it in the response. + +*** + +## 3. Apply the `authenticate` Middleware + +The last step is to apply the `authenticate` middleware on the API routes that require a manager’s authentication. + +To do that, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" highlights={middlewareHighlights} +import { + defineMiddlewares, + authenticate, +} from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/manager", + method: "POST", + middlewares: [ + authenticate("manager", ["session", "bearer"], { + allowUnregistered: true, + }), + ], + }, + { + matcher: "/manager/me*", + middlewares: [ + authenticate("manager", ["session", "bearer"]), + ], + }, + ], +}) +``` + +This applies middlewares on two route patterns: + +1. The `authenticate` middleware is applied on the `/manager` API route for `POST` requests while allowing unregistered managers. This requires that a bearer token be passed in the request to access the manager’s auth identity but doesn’t require the manager to be registered. +2. The `authenticate` middleware is applied on all routes starting with `/manager/me`, restricting these routes to authenticated managers only. + +### Retrieve Manager API Route + +For example, create the file `src/api/manager/me/route.ts` with the following content: + +```ts title="src/api/manager/me/route.ts" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import ManagerModuleService from "../../../modules/manager/service" + +export async function GET( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +): Promise { + const query = req.scope.resolve("query") + const managerId = req.auth_context?.actor_id + + const { data: [manager] } = await query.graph({ + entity: "manager", + fields: ["*"], + filters: { + id: managerId, + }, + }, { + throwIfKeyNotFound: true, + }) + + res.json({ manager }) +} +``` + +This route is only accessible by authenticated managers. You access the manager’s ID using `req.auth_context.actor_id`. + +*** + +## Test Custom Actor Type Authentication Flow + +To authenticate managers: + +1. Send a `POST` request to `/auth/manager/emailpass/register` to create an auth identity for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass/register' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` + +Copy the returned token to use it in the next request. + +2. Send a `POST` request to `/manager` to create a manager: + +```bash +curl -X POST 'http://localhost:9000/manager' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data-raw '{ + "first_name": "John", + "last_name": "Doe", + "email": "manager@gmail.com" +}' +``` + +Replace `{token}` with the token returned in the previous step. + +3. Send a `POST` request to `/auth/manager/emailpass` again to retrieve an authenticated token for the manager: + +```bash +curl -X POST 'http://localhost:9000/auth/manager/emailpass' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "manager@gmail.com", + "password": "supersecret" +}' +``` + +4. You can now send authenticated requests as a manager. For example, send a `GET` request to `/manager/me` to retrieve the authenticated manager’s details: + +```bash +curl 'http://localhost:9000/manager/me' \ +-H 'Authorization: Bearer {token}' +``` + +Whenever you want to log in as a manager, use the `/auth/manager/emailpass` API route, as explained in step 3. + +*** + +## Delete User of Actor Type + +When you delete a user of the actor type, you must update its auth identity to remove the association to the user. + +For example, create the following workflow that deletes a manager and updates its auth identity, create the file `src/workflows/delete-manager.ts` with the following content: + +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import ManagerModuleService from "../modules/manager/service" + +export type DeleteManagerWorkflow = { + id: string +} + +const deleteManagerStep = createStep( + "delete-manager-step", + async ( + { id }: DeleteManagerWorkflow, + { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") + + const manager = await managerModuleService.retrieve(id) + + await managerModuleService.deleteManagers(id) + + return new StepResponse(undefined, { manager }) + }, + async ({ manager }, { container }) => { + const managerModuleService: ManagerModuleService = + container.resolve("manager") + + await managerModuleService.createManagers(manager) + } + ) +``` + +You add a step that deletes the manager using the `deleteManagers` method of the module's main service. In the compensation function, you create the manager again. + +Next, in the same file, add the workflow that deletes a manager: + +```ts title="src/workflows/delete-manager.ts" collapsibleLines="1-15" expandButtonLabel="Show Imports" highlights={deleteHighlights} +// other imports +import { MedusaError } from "@medusajs/framework/utils" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + setAuthAppMetadataStep, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +// ... + +export const deleteManagerWorkflow = createWorkflow( + "delete-manager", + ( + input: WorkflowData + ): WorkflowResponse => { + deleteManagerStep(input) + + const { data: authIdentities } = useQueryGraphStep({ + entity: "auth_identity", + fields: ["id"], + filters: { + app_metadata: { + // the ID is of the format `{actor_type}_id`. + manager_id: input.id, + }, + }, + }) + + const authIdentity = transform( + { authIdentities }, + ({ authIdentities }) => { + const authIdentity = authIdentities[0] + + if (!authIdentity) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + "Auth identity not found" + ) + } + + return authIdentity + } + ) + + setAuthAppMetadataStep({ + authIdentityId: authIdentity.id, + actorType: "manager", + value: null, + }) + + return new WorkflowResponse(input.id) + } +) +``` + +In the workflow, you: + +1. Use the `deleteManagerStep` defined earlier to delete the manager. +2. Retrieve the auth identity of the manager using Query. To do that, you filter the `app_metadata` property of an auth identity, which holds the user's ID under `{actor_type_name}_id`. So, in this case, it's `manager_id`. +3. Check that the auth identity exist, then, update the auth identity to remove the ID of the manager from it. + +You can use this workflow when deleting a manager, such as in an API route. + + +# Auth Module Options + +In this document, you'll learn about the options of the Auth Module. + +## providers + +The `providers` option is an array of auth module providers. + +When the Medusa application starts, these providers are registered and can be used to handle authentication. + +By default, the `emailpass` provider is registered to authenticate customers and admin users. + +For example: + +```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: [ + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // provider 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. + +*** + +## Auth CORS + +The Medusa application's authentication API routes are defined under the `/auth` prefix that requires setting the `authCors` property of the `http` configuration. + +By default, the Medusa application you created will have an `AUTH_CORS` environment variable, which is used as the value of `authCors`. + +Refer to [Medusa's configuration guide](https://docs.medusajs.com/docs/learn/configurations/medusa-config#httpauthCors/index.html.md) to learn more about the `authCors` configuration. + +*** + +## authMethodsPerActor Configuration + +The Medusa application's configuration accept an `authMethodsPerActor` configuration which restricts the allowed auth providers used with an actor type. + +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). + + +# 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). + +Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/reset-password/index.html.md) to learn how to reset your user admin password using the dashboard. + +You'll create a subscriber that listens to the event. When the event is emitted, the subscriber sends an email notification to the user. + +### Prerequisites + +- [A notification provider module, such as SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) + +## 1. Create Subscriber + +The first step is to create a subscriber that listens to the `auth.password_reset` and sends the user a notification with instructions to reset their password. + +Create the file `src/subscribers/handle-reset.ts` with the following content: + +```ts title="src/subscribers/handle-reset.ts" highlights={highlights} collapsibleLines="1-6" expandMoreLabel="Show Imports" +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { Modules } from "@medusajs/framework/utils" + +export default async function resetPasswordTokenHandler({ + event: { data: { + entity_id: email, + token, + actor_type, + } }, + container, +}: SubscriberArgs<{ entity_id: string, token: string, actor_type: string }>) { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const urlPrefix = actor_type === "customer" ? + "https://storefront.com" : + "https://admin.com/app" + + await notificationModuleService.createNotifications({ + to: email, + channel: "email", + template: "reset-password-template", + data: { + // a URL to a frontend application + url: `${urlPrefix}/reset-password?token=${token}&email=${email}`, + }, + }) +} + +export const config: SubscriberConfig = { + event: "auth.password_reset", +} +``` + +You subscribe to the `auth.password_reset` event. The event has a data payload object with the following properties: + +- `entity_id`: The identifier of the user. When using the `emailpass` provider, it's the user's email. +- `token`: The token to reset the user's password. +- `actor_type`: The user's actor type. For example, if the user is a customer, the `actor_type` is `customer`. If it's an admin user, the `actor_type` is `user`. + +This event's payload previously had an `actorType` field. It was renamed to `actor_type` after [Medusa v2.0.7](https://github.com/medusajs/medusa/releases/tag/v2.0.7). + +In the subscriber, you: + +- Decide the frontend URL based on whether the user is a customer or admin user by checking the value of `actor_type`. +- Resolve the Notification Module and use its `createNotifications` method to send the notification. +- You pass to the `createNotifications` method an object having the following properties: + - `to`: The identifier to send the notification to, which in this case is the email. + - `channel`: The channel to send the notification through, which in this case is email. + - `template`: The template ID in the third-party service. + - `data`: The data payload to pass to the template. You pass the URL to redirect the user to. You must pass the token and email in the URL so that the frontend can send them later to the Medusa application when reseting the password. + +*** + +## 2. Test it Out: Generate Reset Password Token + +To test the subscriber out, send a request to the `/auth/{actor_type}/{auth_provider}/reset-password` API route, replacing `{actor_type}` and `{auth_provider}` with the user's actor type and provider used for authentication respectively. + +For example, to generate a reset password token for an admin user using the `emailpass` provider, send the following request: + +```bash +curl --location 'http://localhost:9000/auth/user/emailpass/reset-password' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "identifier": "admin-test@gmail.com" +}' +``` + +In the request body, you must pass an `identifier` parameter. Its value is the user's identifier, which is the email in this case. + +If the token is generated successfully, the request returns a response with `201` status code. In the terminal, you'll find the following message indicating that the `auth.password_reset` event was emitted and your subscriber ran: + +```plain +info: Processing auth.password_reset which has 1 subscribers +``` + +The notification is sent to the user with the frontend URL to enter a new password. + +*** + +## Next Steps: Implementing Frontend + +In your frontend, you must have a page that accepts `token` and `email` query parameters. + +The page shows the user password fields to enter their new password, then submits the new password, token, and email to the [Reset Password Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#reset-password-route/index.html.md). + +### Examples + +- [Storefront Guide: Reset Customer Password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/reset-password/index.html.md) + + # Stock Location Concepts In this document, you’ll learn about the main concepts in the Stock Location Module. @@ -29846,6 +29198,322 @@ createRemoteLinkStep({ ``` +# 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 +``` + + +# Tax Module Options + +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 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 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: + +```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 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. + + +# 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. + + # User Module Options In this document, you'll learn about the options of the User Module. @@ -29963,43 +29631,85 @@ if (!count) { ``` -# Links between Store Module and Other Modules +# Cart Concepts -This document showcases the module links defined between the Store Module and other Commerce Modules. +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. ## Summary -The Store Module has the following links to other modules: +The Cart Module has the following links to other modules: Read-only links are used to query data across modules, but the relations aren't stored in a pivot table in the database. |First Data Model|Second Data Model|Type|Description| |---|---|---|---| -|StoreCurrency|Currency|Read-only - has many|Learn more| +|Cart|Customer|Read-only - has one|Learn more| +|Order|Cart|Stored - one-to-one|Learn more| +|Cart|PaymentCollection|Stored - one-to-one|Learn more| +|LineItem|Product|Read-only - has one|Learn more| +|LineItem|ProductVariant|Read-only - has one|Learn more| +|Cart|Promotion|Stored - many-to-many|Learn more| +|Cart|Region|Read-only - has one|Learn more| +|Cart|SalesChannel|Read-only - has one|Learn more| *** -## Currency Module +## Customer 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). +Medusa defines a read-only link between the `Cart` data model and the [Customer Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/customer/index.html.md)'s `Customer` data model. This means you can retrieve the details of a cart's customer, but you don't manage the links in a pivot table in the database. The customer of a cart is determined by the `customer_id` property of the `Cart` data model. ### Retrieve with Query -To retrieve the details of a store's currencies with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `supported_currencies.currency.*` in `fields`: +To retrieve the customer of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `customer.*` in `fields`: ### query.graph ```ts -const { data: stores } = await query.graph({ - entity: "store", +const { data: carts } = await query.graph({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "customer.*", ], }) -// stores[0].supported_currencies +// carts[0].customer ``` ### useQueryGraphStep @@ -30009,505 +29719,665 @@ import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -const { data: stores } = useQueryGraphStep({ - entity: "store", +const { data: carts } = useQueryGraphStep({ + entity: "cart", fields: [ - "supported_currencies.currency.*", + "customer.*", ], }) -// stores[0].supported_currencies +// carts[0].customer ``` +*** -# Tax Module Options +## Order Module -In this guide, you'll learn about the options of the Tax Module. +The [Order Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/order/index.html.md) provides order-management features. -## providers +Medusa defines a link between the `Cart` and `Order` data models. The cart is linked to the order created once the cart is completed. -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. +![A diagram showcasing an example of how data models from the Cart and Order modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1728375735/Medusa%20Resources/cart-order_ijwmfs.jpg) -When the Medusa application starts, these providers are registered and can be used to retrieve tax lines. +### Retrieve with Query -```ts title="medusa-config.ts" +To retrieve the order of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `order.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts[0].order +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "order.*", + ], +}) + +// carts[0].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" // ... -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/tax", - options: { - providers: [ - { - resolve: "./path/to/my-provider", - id: "my-provider", - options: { - // ... - }, - }, - ], - }, - }, - ], +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, }) ``` -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: +### createRemoteLinkStep ```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.ORDER]: { + order_id: "order_123", + }, +}) +``` + +*** + +## Payment Module + +The [Payment Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/payment/index.html.md) handles payment processing and management. + +Medusa defines a link between the `Cart` and `PaymentCollection` data models. A cart has a payment collection which holds all the authorized payment sessions and payments made related to the cart. + +![A diagram showcasing an example of how data models from the Cart and Payment modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711537849/Medusa%20Resources/cart-payment_ixziqm.jpg) + +### Retrieve with Query + +To retrieve the payment collection of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `payment_collection.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts[0].payment_collection +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "payment_collection.*", + ], +}) + +// carts[0].payment_collection +``` + +### 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", + }, +}) +``` + +*** + +## Product Module + +Medusa defines read-only links between: + +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `Product` data model. This means you can retrieve the details of a line item's product, but you don't manage the links in a pivot table in the database. The product of a line item is determined by the `product_id` property of the `LineItem` data model. +- the `LineItem` data model and the [Product Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/product/index.html.md)'s `ProductVariant` data model. This means you can retrieve the details of a line item's variant, but you don't manage the links in a pivot table in the database. The variant of a line item is determined by the `variant_id` property of the `LineItem` data model. + +### Retrieve with Query + +To retrieve the variant of a line item with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `variant.*` in `fields`: + +To retrieve the product, pass `product.*` in `fields`. + +### query.graph + +```ts +const { data: lineItems } = await query.graph({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: lineItems } = useQueryGraphStep({ + entity: "line_item", + fields: [ + "variant.*", + ], +}) + +// lineItems.variant +``` + +*** + +## Promotion Module + +The [Promotion Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/index.html.md) provides discount features. + +Medusa defines a link between the `Cart` and `Promotion` data models. This indicates the promotions applied on a cart. + +![A diagram showcasing an example of how data models from the Cart and Promotion modules are linked](https://res.cloudinary.com/dza7lstvk/image/upload/v1711538015/Medusa%20Resources/cart-promotion_kuh9vm.jpg) + +Medusa also defines a read-only link between the `LineItemAdjustment` and `Promotion` data models. This means you can retrieve the details of the promotion applied on a line item, but you don't manage the links in a pivot table in the database. The promotion of a line item is determined by the `promotion_id` property of the `LineItemAdjustment` data model. + +### Retrieve with Query + +To retrieve the promotions of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `promotions.*` in `fields`: + +To retrieve the promotion of a line item adjustment, pass `promotion.*` in `fields`. + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts[0].promotions +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "promotions.*", + ], +}) + +// carts[0].promotions +``` + +### Manage with Link + +To manage the promotions of a cart, use [Link](https://docs.medusajs.com/docs/learn/fundamentals/module-links/link/index.html.md): + +### link.create + +```ts +import { Modules } from "@medusajs/framework/utils" + +// ... + +await link.create({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +### createRemoteLinkStep + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createRemoteLinkStep } from "@medusajs/medusa/core-flows" + +// ... + +createRemoteLinkStep({ + [Modules.CART]: { + cart_id: "cart_123", + }, + [Modules.PROMOTION]: { + promotion_id: "promo_123", + }, +}) +``` + +*** + +## Region Module + +Medusa defines a read-only link between the `Cart` data model and the [Region Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/region/index.html.md)'s `Region` data model. This means you can retrieve the details of a cart's region, but you don't manage the links in a pivot table in the database. The region of a cart is determined by the `region_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the region of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `region.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts[0].region +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "region.*", + ], +}) + +// carts[0].region +``` + +*** + +## Sales Channel Module + +Medusa defines a read-only link between the `Cart` data model and the [Sales Channel Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/sales-channel/index.html.md)'s `SalesChannel` data model. This means you can retrieve the details of a cart's sales channel, but you don't manage the links in a pivot table in the database. The sales channel of a cart is determined by the `sales_channel_id` property of the `Cart` data model. + +### Retrieve with Query + +To retrieve the sales channel of a cart with [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), pass `sales_channel.*` in `fields`: + +### query.graph + +```ts +const { data: carts } = await query.graph({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts[0].sales_channel +``` + +### useQueryGraphStep + +```ts +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" + +// ... + +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "sales_channel.*", + ], +}) + +// carts[0].sales_channel +``` + + +# Tax Lines in Cart Module + +In this document, you’ll learn about tax lines in a cart and how to retrieve tax lines with the Tax Module. + +## What are Tax Lines? + +A tax line indicates the tax rate of a line item or a shipping method. The [LineItemTaxLine data model](https://docs.medusajs.com/references/cart/models/LineItemTaxLine/index.html.md) represents a line item’s tax line, and the [ShippingMethodTaxLine data model](https://docs.medusajs.com/references/cart/models/ShippingMethodTaxLine/index.html.md) represents a shipping method’s tax line. + +![A diagram showcasing the relation between other data models and the tax line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534431/Medusa%20Resources/cart-tax-lines_oheaq6.jpg) + +*** + +## Tax Inclusivity + +By default, the tax amount is calculated by taking the tax rate from the line item or shipping method’s amount, and then adding them to the item/method’s subtotal. + +However, line items and shipping methods have an `is_tax_inclusive` property that, when enabled, indicates that the item or method’s price already includes taxes. + +So, instead of calculating the tax rate and adding it to the item/method’s subtotal, it’s calculated as part of the subtotal. + +The following diagram is a simplified showcase of how a subtotal is calculated from the taxes perspective. + +![A diagram showing an example of calculating the subtotal of a line item using its taxes](https://res.cloudinary.com/dza7lstvk/image/upload/v1711535295/Medusa%20Resources/cart-tax-inclusive_shpr3t.jpg) + +For example, if a line item's amount is `5000`, the tax rate is `10`, and tax inclusivity is enabled, the tax amount is 10% of `5000`, which is `500`, making the unit price of the line item `4500`. + +*** + +## Retrieve Tax Lines + +When using the Cart and Tax modules together, you can use the `getTaxLines` method of the Tax Module’s main service. It retrieves the tax lines for a cart’s line items and shipping methods. + +```ts +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.tax_lines", + "shipping_methods.tax_lines", + "shipping_address", + ], +}) + +// retrieve the tax lines const taxLines = await taxModuleService.getTaxLines( [ - { - id: "cali_123", - product_id: "prod_123", - unit_price: 1000, - quantity: 1, - }, - { - id: "casm_123", - shipping_option_id: "so_123", - unit_price: 2000, - }, + ...(cart.items as TaxableItemDTO[]), + ...(cart.shipping_methods as TaxableShippingDTO[]), ], { address: { - country_code: "us", + ...cart.shipping_address, + country_code: + cart.shipping_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. +Then, use the returned tax lines to set the line items and shipping methods’ tax lines: -The example above retrieves the tax lines based on the tax region for the United States. +```ts +// set line item tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "line_item_id" in line) +) -The method returns tax lines for the line item and shipping methods. For example: +// set shipping method tax lines +await cartModuleService.setLineItemTaxLines( + cart.id, + taxLines.filter((line) => "shipping_line_id" in line) +) +``` -```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" + +# Promotions Adjustments in Carts + +In this document, you’ll learn how a promotion is applied to a cart’s line items and shipping methods using adjustment lines. + +## What are Adjustment Lines? + +An adjustment line indicates a change to an item or a shipping method’s amount. It’s used to apply promotions or discounts on a cart. + +The [LineItemAdjustment](https://docs.medusajs.com/references/cart/models/LineItemAdjustment/index.html.md) data model represents changes on a line item, and the [ShippingMethodAdjustment](https://docs.medusajs.com/references/cart/models/ShippingMethodAdjustment/index.html.md) data model represents changes on a shipping method. + +![A diagram showcasing the relations between other data models and adjustment line models](https://res.cloudinary.com/dza7lstvk/image/upload/v1711534248/Medusa%20Resources/cart-adjustments_k4sttb.jpg) + +The `amount` property of the adjustment line indicates the amount to be discounted from the original amount. Also, the ID of the applied promotion is stored in the `promotion_id` property of the adjustment line. + +*** + +## Discountable Option + +The [LineItem](https://docs.medusajs.com/references/cart/models/LineItem/index.html.md) data model has an `is_discountable` property that indicates whether promotions can be applied to the line item. It’s enabled by default. + +When disabled, a promotion can’t be applied to a line item. In the context of the Promotion Module, the promotion isn’t applied to the line item even if it matches its rules. + +*** + +## Promotion Actions + +When using the Cart and Promotion modules together, such as in the Medusa application, use the [computeActions method of the Promotion Module’s main service](https://docs.medusajs.com/references/promotion/computeActions/index.html.md). It retrieves the actions of line items and shipping methods. + +Learn more about actions in the [Promotion Module’s documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/promotion/actions/index.html.md). + +For example: + +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + ComputeActionAdjustmentLine, + ComputeActionItemLine, + ComputeActionShippingLine, + // ... +} from "@medusajs/framework/types" + +// retrieve the cart +const cart = await cartModuleService.retrieveCart("cart_123", { + relations: [ + "items.adjustments", + "shipping_methods.adjustments", + ], +}) + +// retrieve line item adjustments +const lineItemAdjustments: ComputeActionItemLine[] = [] +cart.items.forEach((item) => { + const filteredAdjustments = item.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + lineItemAdjustments.push({ + ...item, + adjustments: filteredAdjustments, + }) } -] +}) + +// retrieve shipping method adjustments +const shippingMethodAdjustments: ComputeActionShippingLine[] = + [] +cart.shipping_methods.forEach((shippingMethod) => { + const filteredAdjustments = + shippingMethod.adjustments?.filter( + (adjustment) => adjustment.code !== undefined + ) as unknown as ComputeActionAdjustmentLine[] + if (filteredAdjustments.length) { + shippingMethodAdjustments.push({ + ...shippingMethod, + adjustments: filteredAdjustments, + }) + } +}) + +// compute actions +const actions = await promotionModuleService.computeActions( + ["promo_123"], + { + items: lineItemAdjustments, + shipping_methods: shippingMethodAdjustments, + } +) ``` -*** +The `computeActions` method accepts the existing adjustments of line items and shipping methods to compute the actions accurately. -## Using the Tax Provider in the Calculation +Then, use the returned `addItemAdjustment` and `addShippingMethodAdjustment` actions to set the cart’s line item and the shipping method’s adjustments. -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 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 Rates and Rules - -In this document, you’ll learn about tax rates and rules. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions#manage-tax-rate-overrides/index.html.md) to learn how to manage tax rates using the dashboard. - -## What are Tax Rates? - -A tax rate is a percentage amount used to calculate the tax amount for each taxable item’s price, such as line items or shipping methods, in a cart. The sum of all calculated tax amounts are then added to the cart’s total as a tax total. - -Each tax region has a default tax rate. This tax rate is applied to all taxable items of a cart in that region. - -### Combinable Tax Rates - -Tax regions can have parent tax regions. To inherit the tax rates of the parent tax region, set the `is_combinable` of the child’s tax rates to `true`. - -Then, when tax rates are retrieved for a taxable item in the child region, both the child and the parent tax regions’ applicable rates are returned. - -*** - -## Override Tax Rates with Rules - -You can create tax rates that override the default for specific conditions or rules. - -For example, you can have a default tax rate is 10%, but for products of type “Shirt” is %15. - -A tax region can have multiple tax rates, and each tax rate can have multiple tax rules. The [TaxRateRule data model](https://docs.medusajs.com/references/tax/models/TaxRateRule/index.html.md) represents a tax rate’s rule. - -![A diagram showcasing the relation between TaxRegion, TaxRate, and TaxRateRule](https://res.cloudinary.com/dza7lstvk/image/upload/v1711462167/Medusa%20Resources/tax-rate-rule_enzbp2.jpg) - -These two properties of the data model identify the rule’s target: - -- `reference`: the name of the table in the database that this rule points to. For example, `product_type`. -- `reference_id`: the ID of the data model’s record that this points to. For example, a product type’s ID. - -So, to override the default tax rate for product types “Shirt”, you create a tax rate and associate with it a tax rule whose `reference` is `product_type` and `reference_id` the ID of the “Shirt” product type. - - -# Tax Region - -In this document, you’ll learn about tax regions and how to use them with the Region Module. - -Refer to this [Medusa Admin User Guide](https://docs.medusajs.com/user-guide/settings/tax-regions/index.html.md) to learn how to manage tax regions using the dashboard. - -## What is a Tax Region? - -A tax region, represented by the [TaxRegion data model](https://docs.medusajs.com/references/tax/models/TaxRegion/index.html.md), stores tax settings related to a region that your store serves. - -Tax regions can inherit settings and rules from a parent tax region. - -*** - -## 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. - - -# 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" +```ts collapsibleLines="1-8" expandButtonLabel="Show Imports" +import { + AddItemAdjustmentAction, + AddShippingMethodAdjustment, + // ... +} from "@medusajs/framework/types" // ... -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, - }, - }, - ], - }, - }, - }, +await cartModuleService.setLineItemAdjustments( + cart.id, + actions.filter( + (action) => action.action === "addItemAdjustment" + ) as AddItemAdjustmentAction[] +) + +await cartModuleService.setShippingMethodAdjustments( + cart.id, + actions.filter( + (action) => + action.action === "addShippingMethodAdjustment" + ) as AddShippingMethodAdjustment[] +) +``` + + +# Get Product Variant Prices using Query + +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. + +So, to retrieve data across the linked records of the two modules, you use Query. + +## Retrieve All Product Variant Prices + +To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. + +For example: + +```ts highlights={[["6"]]} +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.prices.*", ], + filters: { + id: [ + "prod_123", + ], + }, }) ``` -### 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| +Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). *** -*** +## Retrieve Calculated Price for a Context -## Override Callback URL During Authentication +The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. -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. +Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). -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). +To retrieve calculated prices of variants based on a context, retrieve the products using Query and: -*** +- Pass `variants.calculated_price.*` in the `fields` property. +- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. -## Examples +For example: -- [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). - - -# GitHub Auth Module Provider - -In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. - -The Github Auth Module Provider authenticates users with their GitHub account. - -Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). - -*** - -## Register the Github Auth Module Provider - -### Prerequisites - -- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) -- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) - -Add the module to the array of providers passed to the Auth Module: - -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" +```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} +import { QueryContext } 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, - }, - }, - ], - }, - }, +const { data: products } = await query.graph({ + entity: "product", + fields: [ + "*", + "variants.*", + "variants.calculated_price.*", ], + filters: { + id: "prod_123", + }, + context: { + variants: { + calculated_price: QueryContext({ + region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", + currency_code: "eur", + }), + }, + }, }) ``` -### Environment Variables +For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. -Make sure to add the necessary environment variables for the above options in `.env`: +`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). -```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). - - -# Emailpass Auth Module Provider - -In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. - -Using the Emailpass auth module provider, you allow users to register and login with an email and password. - -*** - -## Register the Emailpass Auth Module Provider - -The Emailpass auth provider is registered by default with the Auth Module. - -If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: - -```ts title="medusa-config.ts" -import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/auth", - dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], - options: { - providers: [ - // other providers... - { - resolve: "@medusajs/medusa/auth-emailpass", - id: "emailpass", - options: { - // options... - }, - }, - ], - }, - }, - ], -}) -``` - -### Module Options - -|Configuration|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`hashConfig\`|An object of configurations to use when hashing the user's -password. Refer to |No|\`\`\`ts -const hashConfig = \{ - logN: 15, - r: 8, - p: 1 -} -\`\`\`| - -*** - -## Related Guides - -- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) +Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). # Stripe Module Provider @@ -30640,86 +30510,6 @@ When you set up the webhook in Stripe, choose the following events to listen to: - [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 Prices using Query - -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). - -The Product Module doesn't provide pricing functionalities. The Medusa application links the Product Module's `ProductVariant` data model to the Pricing Module's `PriceSet` data model. - -So, to retrieve data across the linked records of the two modules, you use Query. - -## Retrieve All Product Variant Prices - -To retrieve all product variant prices, retrieve the product using Query and include among its fields `variants.prices.*`. - -For example: - -```ts highlights={[["6"]]} -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.prices.*", - ], - filters: { - id: [ - "prod_123", - ], - }, -}) -``` - -Each variant in the retrieved products has a `prices` array property with all the product variant prices. Each price object has the properties of the [Pricing Module's Price data model](https://docs.medusajs.com/references/pricing/models/Price/index.html.md). - -*** - -## Retrieve Calculated Price for a Context - -The Pricing Module can calculate prices of a variant based on a [context](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md), such as the region ID or the currency code. - -Learn more about prices calculation in [this Pricing Module documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md). - -To retrieve calculated prices of variants based on a context, retrieve the products using Query and: - -- Pass `variants.calculated_price.*` in the `fields` property. -- Pass a `context` property in the object parameter. Its value is an object of objects that sets the context for the retrieved fields. - -For example: - -```ts highlights={[["10"], ["15"], ["16"], ["17"], ["18"], ["19"], ["20"], ["21"], ["22"]]} -import { QueryContext } from "@medusajs/framework/utils" - -// ... - -const { data: products } = await query.graph({ - entity: "product", - fields: [ - "*", - "variants.*", - "variants.calculated_price.*", - ], - filters: { - id: "prod_123", - }, - context: { - variants: { - calculated_price: QueryContext({ - region_id: "reg_01J3MRPDNXXXDSCC76Y6YCZARS", - currency_code: "eur", - }), - }, - }, -}) -``` - -For the context of the product variant's calculated price, you pass an object to `context` with the property `variants`, whose value is another object with the property `calculated_price`. - -`calculated_price`'s value is created using `QueryContext` from the Modules SDK, passing it a [calculation context object](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#calculation-context/index.html.md). - -Each variant in the retrieved products has a `calculated_price` object. Learn more about its properties in [this Pricing Module guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation#returned-price-object/index.html.md). - - # Calculate Product Variant Price with Taxes In this document, you'll learn how to calculate a product variant's price with taxes. @@ -31058,6 +30848,237 @@ 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. +# Emailpass Auth Module Provider + +In this document, you’ll learn about the Emailpass auth module provider and how to install and use it in the Auth Module. + +Using the Emailpass auth module provider, you allow users to register and login with an email and password. + +*** + +## Register the Emailpass Auth Module Provider + +The Emailpass auth provider is registered by default with the Auth Module. + +If you want to pass options to the provider, add the provider to the `providers` option of the Auth Module: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + options: { + // options... + }, + }, + ], + }, + }, + ], +}) +``` + +### Module Options + +|Configuration|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`hashConfig\`|An object of configurations to use when hashing the user's +password. Refer to |No|\`\`\`ts +const hashConfig = \{ + logN: 15, + r: 8, + p: 1 +} +\`\`\`| + +*** + +## Related Guides + +- [How to register a customer using email and password](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/register/index.html.md) + + +# Google Auth Module Provider + +In this document, you’ll learn about the Google Auth Module Provider and how to install and use it in the Auth Module. + +The Google Auth Module Provider authenticates users with their Google account. + +Learn about the authentication flow for third-party providers in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#2-third-party-service-authenticate-flow/index.html.md). + +*** + +## Register the Google Auth Module Provider + +### Prerequisites + +- [Create a project in Google Cloud.](https://cloud.google.com/resource-manager/docs/creating-managing-projects) +- [Create authorization credentials. When setting the Redirect Uri, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred) + +Add the module to the array of providers passed to the Auth Module: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + // ... + [Modules.AUTH]: { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-google", + id: "google", + options: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackUrl: process.env.GOOGLE_CALLBACK_URL, + }, + }, + ], + }, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_CALLBACK_URL= +``` + +### Module Options + +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the |Yes| +|\`clientSecret\`|A string indicating the |Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in Google.|Yes| + +*** + +*** + +## Override Callback URL During Authentication + +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. + +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). + +*** + +## Examples + +- [How to implement Google social login in the storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). + + +# GitHub Auth Module Provider + +In this document, you’ll learn about the GitHub Auth Module Provider and how to install and use it in the Auth Module. + +The Github Auth Module Provider authenticates users with their GitHub account. + +Learn about the authentication flow in [this guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route/index.html.md). + +*** + +## Register the Github Auth Module Provider + +### Prerequisites + +- [Register GitHub App. When setting the Callback URL, set it to a URL in your frontend that later uses Medusa's callback route to validate the authentication.](https://docs.github.com/en/apps/creating-github-apps/setting-up-a-github-app/creating-a-github-app) +- [Retrieve the client ID and client secret of your GitHub App](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#using-basic-authentication) + +Add the module to the array of providers passed to the Auth Module: + +```ts title="medusa-config.ts" +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [Modules.CACHE, ContainerRegistrationKeys.LOGGER], + options: { + providers: [ + // other providers... + { + resolve: "@medusajs/medusa/auth-github", + id: "github", + options: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET, + callbackUrl: process.env.GITHUB_CALLBACK_URL, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the necessary environment variables for the above options in `.env`: + +```plain +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_CALLBACK_URL= +``` + +### Module Options + +|Configuration|Description|Required| +|---|---|---|---|---| +|\`clientId\`|A string indicating the client ID of your GitHub app.|Yes| +|\`clientSecret\`|A string indicating the client secret of your GitHub app.|Yes| +|\`callbackUrl\`|A string indicating the URL to redirect to in your frontend after the user completes their authentication in GitHub.|Yes| + +*** + +## Override Callback URL During Authentication + +In many cases, you may have different callback URL for actor types. For example, you may redirect admin users to a different URL than customers after authentication. + +The [Authenticate or Login API Route](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md) can accept a `callback_url` body parameter to override the provider's `callbackUrl` option. Learn more in [this documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/authentication-route#login-route/index.html.md). + +*** + +## Examples + +- [How to implement third-party / social login in the storefront.](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/storefront-development/customers/third-party-login/index.html.md). + + # Infrastructure Modules Medusa's architectural functionalities, such as emitting and subscribing to events or caching data, are all implemented in Infrastructure Modules. An Infrastructure Module is a package that can be installed and used in any Medusa application. These modules allow you to choose and integrate custom services for architectural purposes. @@ -31375,6 +31396,142 @@ This is useful for development. However, for production, it’s highly recommend - [AWS S3 (and Compatible APIs)](https://docs.medusajs.com/infrastructure-modules/file/s3/index.html.md) +# Cache Module + +In this document, you'll learn what a Cache Module is and how to use it in your Medusa application. + +## What is a Cache Module? + +A Cache Module is used to cache the results of computations such as price selection or various tax calculations. + +The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture. + +### Default Cache Module + +By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). + +*** + +## How to Use the Cache Module? + +You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const cacheModuleService = container.resolve( + Modules.CACHE + ) + + await cacheModuleService.set("key", "value") + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`. + +*** + +## List of Cache Modules + +Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) + + +# Event Module + +In this document, you'll learn what an Event Module is and how to use it in your Medusa application. + +## What is an Event Module? + +An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. + +This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own. + +Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +### Default Event Module + +By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). + +*** + +## How to Use the Event Module? + +You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const eventModuleService = container.resolve( + Modules.EVENT + ) + + await eventModuleService.emit({ + name: "custom.event", + data: { + id: "123", + // other data payload + }, + }) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + step1() + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`. + +*** + +## List of Event Modules + +Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). + +- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) + + # Locking Module In this document, you'll learn about the Locking Module and its providers. @@ -31488,29 +31645,91 @@ Medusa provides the following Locking Module Providers. You can use one of them, - [PostgreSQL](https://docs.medusajs.com/infrastructure-modules/locking/postgres/index.html.md) -# Event Module +# Workflow Engine Module -In this document, you'll learn what an Event Module is and how to use it in your Medusa application. +In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. -## What is an Event Module? +## What is a Workflow Engine Module? -An Event Module implements the underlying publish/subscribe system that handles queueing events, emitting them, and executing their subscribers. +A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service. -This makes the event architecture customizable, as you can either choose one of Medusa’s event modules or create your own. +### Default Workflow Engine Module -Learn more about Medusa's event systems in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -### Default Event Module - -By default, Medusa uses the [Local Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/local/index.html.md). This module uses Node’s EventEmitter to implement the publish/subscribe system. While this is suitable for development, it's recommended to use other Event Modules, such as the [Redis Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/redis/index.html.md), for production. You can also [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). +Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead. *** -## How to Use the Event Module? +## How to Use the Workflow Engine Module? -You can use the registered Event Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. -Medusa provides the helper step [emitEventStep](https://docs.medusajs.com/references/helper-steps/emitEventStep/index.html.md) that you can use in your workflow. You can also resolve the Event Module's service in a step of your workflow and use its methods to emit events. +In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps. + +For example: + +```ts +import { Modules } from "@medusajs/framework/utils" +import { + createStep, + createWorkflow, + StepResponse, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const workflowEngineService = container.resolve( + Modules.WORKFLOW_ENGINE + ) + + const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ + transaction_id: transaction_id, + }) + + return new StepResponse(workflowExecution) + } +) + +export const workflow = createWorkflow( + "workflow-1", + () => { + const workflowExecution = step1() + + return new WorkflowResponse(workflowExecution) + } +) +``` + +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow. + +*** + +## List of Workflow Engine Modules + +Medusa provides the following Workflow Engine Modules. + +- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) +- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) + + +# Notification Module + +In this document, you'll learn about the Notification Module and its providers. + +## What is the Notification Module? + +The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well. + +*** + +## How to Use the Notification Module? + +You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. + +In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications. For example: @@ -31524,16 +31743,15 @@ import { const step1 = createStep( "step-1", async ({}, { container }) => { - const eventModuleService = container.resolve( - Modules.EVENT + const notificationModuleService = container.resolve( + Modules.NOTIFICATION ) - await eventModuleService.emit({ - name: "custom.event", - data: { - id: "123", - // other data payload - }, + await notificationModuleService.createNotifications({ + to: "customer@gmail.com", + channel: "email", + template: "product-created", + data, }) } ) @@ -31546,82 +31764,64 @@ export const workflow = createWorkflow( ) ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Event Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). -Then, you use the `emit` method of the Event Module to emit an event with the name `"custom.event"` and the data payload `{ id: "123" }`. +Then, you use the `createNotifications` method of the Notification Module to send an email notification. + +Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). *** -## List of Event Modules +## What is a Notification Module Provider? -Medusa provides the following Event Modules. You can use one of them, or [Create an Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/create/index.html.md). +A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification. -- [Local](https://docs.medusajs.com/infrastructure-modules/event/local/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/event/redis/index.html.md) +By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal. +Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). -# Cache Module - -In this document, you'll learn what a Cache Module is and how to use it in your Medusa application. - -## What is a Cache Module? - -A Cache Module is used to cache the results of computations such as price selection or various tax calculations. - -The underlying database, third-party service, or caching logic is flexible since it's implemented in a module. You can choose from Medusa’s cache modules or create your own to support something more suitable for your architecture. - -### Default Cache Module - -By default, Medusa uses the [In-Memory Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/in-memory/index.html.md). This module uses a plain JavaScript Map object to store the cache data. While this is suitable for development, it's recommended to use other Cache Modules, such as the [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md), for production. You can also [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). +- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) +- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) *** -## How to Use the Cache Module? +## Notification Module Provider Channels -You can use the registered Cache Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +When you send a notification, you specify the channel to send it through, such as `email` or `sms`. -In a step of your workflow, you can resolve the Cache Module's service and use its methods to cache data, retrieve cached data, or clear the cache. +You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel. For example: -```ts +```ts title="medusa-config.ts" highlights={[["19"]]} import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, -} from "@medusajs/framework/workflows-sdk" -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const cacheModuleService = container.resolve( - Modules.CACHE - ) +// ... - await cacheModuleService.set("key", "value") - } -) - -export const workflow = createWorkflow( - "workflow-1", - () => { - step1() - } -) +module.exports = { + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // ... + { + resolve: "@medusajs/medusa/notification-local", + id: "notification", + options: { + channels: ["email"], + }, + }, + ], + }, + }, + ], +} ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Cache Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). - -Then, you use the `set` method of the Cache Module to cache the value `"value"` with the key `"key"`. - -*** - -## List of Cache Modules - -Medusa provides the following Cache Modules. You can use one of them, or [Create a Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/create/index.html.md). - -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/cache/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/cache/redis/index.html.md) +The `channels` option is an array of strings indicating the channels this provider is used for. # Local Analytics Module Provider @@ -32133,414 +32333,214 @@ module.exports = defineConfig({ ## Troubleshooting -# Workflow Engine Module +# How to Create a Cache Module -In this document, you'll learn what a Workflow Engine Module is and how to use it in your Medusa application. +In this guide, you’ll learn how to create a Cache Module. -## What is a Workflow Engine Module? +## 1. Create Module Directory -A Workflow Engine Module handles tracking and recording the transactions and statuses of workflows and their steps. It can use custom mechanism or integrate a third-party service. - -### Default Workflow Engine Module - -Medusa uses the [In-Memory Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/in-memory/index.html.md) by default. For production purposes, it's recommended to use the [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md) instead. +Start by creating a new directory for your module. For example, `src/modules/my-cache`. *** -## How to Use the Workflow Engine Module? +## 2. Create the Cache Service -You can use the registered Workflow Engine Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. +Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service. -In a step of your workflow, you can resolve the Workflow Engine Module's service and use its methods to track and record the transactions and statuses of workflows and their steps. +The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`: -For example: +```ts title="src/modules/my-cache/service.ts" +import { ICacheService } from "@medusajs/framework/types" -```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, - StepResponse, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const workflowEngineService = container.resolve( - Modules.WORKFLOW_ENGINE - ) - - const [workflowExecution] = await workflowEngineService.listWorkflowExecutions({ - transaction_id: transaction_id, - }) - - return new StepResponse(workflowExecution) - } -) - -export const workflow = createWorkflow( - "workflow-1", - () => { - const workflowExecution = step1() - - return new WorkflowResponse(workflowExecution) +class MyCacheService implements ICacheService { + get(key: string): Promise { + throw new Error("Method not implemented.") } -) + set(key: string, data: unknown, ttl?: number): Promise { + throw new Error("Method not implemented.") + } + invalidate(key: string): Promise { + throw new Error("Method not implemented.") + } +} + +export default MyCacheService ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Workflow Engine Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +The service implements the required methods based on the desired caching mechanism. -Then, you use the `listWorkflowExecutions` method of the Workflow Engine Module to list the workflow executions with the transaction ID `transaction_id`. The workflow execution is then returned as a response from the step and the workflow. +### Implement get Method -*** +The `get` method retrieves the value of a cached item based on its key. -## List of Workflow Engine Modules +The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist. -Medusa provides the following Workflow Engine Modules. +For example, to implement this method using Memcached: -- [In-Memory](https://docs.medusajs.com/infrastructure-modules/workflow-engine/in-memory/index.html.md) -- [Redis](https://docs.medusajs.com/infrastructure-modules/workflow-engine/redis/index.html.md) - - -# Notification Module - -In this document, you'll learn about the Notification Module and its providers. - -## What is the Notification Module? - -The Notification Module exposes the functionalities to send a notification to a customer or user. For example, sending an order confirmation email. Medusa uses the Notification Module in its core commerce features for notification operations, and you an use it in your custom features as well. - -*** - -## How to Use the Notification Module? - -You can use the Notification Module as part of the [workflows](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) you build for your custom features. A workflow is a special function composed of a series of steps that guarantees data consistency and reliable roll-back mechanism. - -In a step of your workflow, you can resolve the Notification Module's service and use its methods to send notifications. - -For example: - -```ts -import { Modules } from "@medusajs/framework/utils" -import { - createStep, - createWorkflow, -} from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) - - await notificationModuleService.createNotifications({ - to: "customer@gmail.com", - channel: "email", - template: "product-created", - data, +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + // ... + async get(cacheKey: string): Promise { + return new Promise((res, rej) => { + this.memcached.get(cacheKey, (err, data) => { + if (err) { + res(null) + } else { + if (data) { + res(JSON.parse(data)) + } else { + res(null) + } + } + }) }) - } -) - -export const workflow = createWorkflow( - "workflow-1", - () => { - step1() } -) +} ``` -In the example above, you create a workflow that has a step. In the step, you resolve the service of the Notification Module from the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +### Implement set Method -Then, you use the `createNotifications` method of the Notification Module to send an email notification. +The `set` method is used to set an item in the cache. It accepts three parameters: -Find a full example of sending a notification in the [Send Notification guide](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). +1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item. +2. The second parameter is the data to be added to the cache. The data can be of any type. +3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache. + +For example, to implement this method using Memcached: + +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + protected TTL = 60 + // ... + async set( + key: string, + data: Record, + ttl: number = this.TTL // or any value + ): Promise { + return new Promise((res, rej) => + this.memcached.set( + key, JSON.stringify(data), ttl, (err) => { + if (err) { + rej(err) + } else { + res() + } + }) + ) + } +} +``` + +### Implement invalidate Method + +The `invalidate` method removes an item from the cache using its key. + +By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand. + +The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache. + +For example, to implement this method using Memcached: + +```ts title="src/modules/my-cache/service.ts" +class MyCacheService implements ICacheService { + // ... + async invalidate(key: string): Promise { + return new Promise((res, rej) => { + this.memcached.del(key, (err) => { + if (err) { + rej(err) + } else { + res() + } + }) + }) + } +} +``` *** -## What is a Notification Module Provider? +## 3. Create Module Definition File -A Notification Module Provider implements the underlying logic of sending notification. It either integrates a third-party service or uses custom logic to send the notification. +Create the file `src/modules/my-cache/index.ts` with the following content: -By default, Medusa uses the [Local Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/local/index.html.md) which only simulates sending the notification by logging a message in the terminal. +```ts title="src/modules/my-cache/index.ts" +import MyCacheService from "./service" +import { Module } from "@medusajs/framework/utils" -Medusa provides other Notification Modules that actually send notifications, such as the [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/send-notification/index.html.md). You can also [Create a Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). +export default Module("my-cache", { + service: MyCacheService, +}) +``` -- [Local](https://docs.medusajs.com/infrastructure-modules/notification/local/index.html.md) -- [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) +This exports the module's definition, indicating that the `MyCacheService` is the main service of the module. *** -## Notification Module Provider Channels +## 4. Use Module -When you send a notification, you specify the channel to send it through, such as `email` or `sms`. - -You register providers of the Notification Module in `medusa-config.ts`. For each provider, you pass a `channels` option specifying which channels the provider can be used in. Only one provider can be setup for each channel. +To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key. For example: -```ts title="medusa-config.ts" highlights={[["19"]]} +```ts title="medusa-config.ts" import { Modules } from "@medusajs/framework/utils" // ... -module.exports = { +module.exports = defineConfig({ // ... modules: [ - // ... { - resolve: "@medusajs/medusa/notification", + resolve: "./src/modules/my-cache", options: { - providers: [ - // ... - { - resolve: "@medusajs/medusa/notification-local", - id: "notification", - options: { - channels: ["email"], - }, - }, - ], + // any options + ttl: 30, }, }, ], -} +}) ``` -The `channels` option is an array of strings indicating the channels this provider is used for. +# In-Memory Cache Module -# Redis Locking Module Provider +The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application. -The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments. +This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. -This provider is recommended for production environments where Medusa is running in a multi-instance setup. +For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md). *** -## Register the Redis Locking Module Provider +## Register the In-Memory Cache Module -### Prerequisites +The In-Memory Cache Module is registered by default in your application. -- [A redis server set up locally or a database in your deployed application.](https://redis.io/download) - -To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: +Add the module into the `modules` property of the exported object in `medusa-config.ts`: ```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-redis", - id: "locking-redis", - // set this if you want this provider to be used by default - // and you have other Locking Module Providers registered. - is_default: true, - options: { - redisUrl: process.env.LOCKING_REDIS_URL, - }, - }, - ], - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the following environment variable: - -```bash -LOCKING_REDIS_URL= -``` - -Where `` is the URL of your Redis server, either locally or in the deployed environment. - -The default Redis URL in a local environment is `redis://localhost:6379`. - -### Redis Locking Module Provider Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| -|\`redisOptions\`|An object of Redis options. Refer to the |No|-| -|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`| -|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`| -|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`| -|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`| - -*** - -## Test out the Module - -To test out the Redis Locking Module Provider, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -You'll see the following message logged in the terminal: - -```bash -info: Connection to Redis in "locking-redis" provider established -``` - -This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server. - -If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations. - -*** - -## Use Provider with Locking Module - -The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: - -```ts title="medusa-config.ts" highlights={defaultHighlights} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-redis", - id: "locking-redis", - is_default: true, - options: { - // ... - }, - }, - ], - }, - }, - ], -}) -``` - -If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods. - -For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): - -```ts import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" +// ... -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING - ) - - await lockingModuleService.acquire("prod_123", { - provider: "lp_locking-redis", - }) - } -) -``` - - -# PostgreSQL Locking Module Provider - -The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database. - -The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks. - -While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible. - -*** - -## Register the PostgreSQL Locking Module Provider - -To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: - -```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/locking", + resolve: "@medusajs/medusa/cache-inmemory", options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-postgres", - id: "locking-postgres", - // set this if you want this provider to be used by default - // and you have other Locking Module Providers registered. - is_default: true, - }, - ], + // optional options }, }, ], }) ``` -### Run Migrations +### In-Memory Cache Module Options -The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider: - -```bash -npx medusa db:migrate -``` - -This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database. - -*** - -## Use Provider with Locking Module - -The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: - -```ts title="medusa-config.ts" highlights={defaultHighlights} -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/locking", - options: { - providers: [ - { - resolve: "@medusajs/medusa/locking-postgres", - id: "locking-postgres", - is_default: true, - }, - ], - }, - }, - ], -}) -``` - -If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods. - -For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): - -```ts -import { Modules } from "@medusajs/framework/utils" -import { createStep } from "@medusajs/framework/workflows-sdk" - -const step1 = createStep( - "step-1", - async ({}, { container }) => { - const lockingModuleService = container.resolve( - Modules.LOCKING - ) - - await lockingModuleService.acquire("prod_123", { - provider: "lp_locking-postgres", - }) - } -) -``` +|Option|Description|Default| +|---|---|---|---|---| +|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`| # How to Create an Event Module @@ -32808,6 +32808,401 @@ Local Event Bus installed. This is not recommended for production. ``` +# Redis Cache Module + +The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module. + +*** + +## Register the Redis Cache Module + +### Prerequisites + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" highlights={highlights} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/cache-redis", + options: { + redisUrl: process.env.CACHE_REDIS_URL, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +CACHE_REDIS_URL= +``` + +### Redis Cache Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| +|\`redisOptions\`|An object of Redis options. Refer to the |No|-| +|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`| +|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`| + +*** + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'cache-redis' established +``` + + +# Redis Locking Module Provider + +The Redis Locking Module Provider uses Redis to manage locks across multiple instances of Medusa. Redis ensures that locks are globally available, which is ideal for distributed environments. + +This provider is recommended for production environments where Medusa is running in a multi-instance setup. + +*** + +## Register the Redis Locking Module Provider + +### Prerequisites + +- [A redis server set up locally or a database in your deployed application.](https://redis.io/download) + +To register the Redis Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-redis", + id: "locking-redis", + // set this if you want this provider to be used by default + // and you have other Locking Module Providers registered. + is_default: true, + options: { + redisUrl: process.env.LOCKING_REDIS_URL, + }, + }, + ], + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variable: + +```bash +LOCKING_REDIS_URL= +``` + +Where `` is the URL of your Redis server, either locally or in the deployed environment. + +The default Redis URL in a local environment is `redis://localhost:6379`. + +### Redis Locking Module Provider Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| +|\`redisOptions\`|An object of Redis options. Refer to the |No|-| +|\`namespace\`|A string used to prefix all locked keys with |No|\`medusa\_lock:\`| +|\`waitLockingTimeout\`|A number indicating the default timeout (in seconds) to wait while acquiring a lock. This timeout is used when no timeout is specified when executing an asynchronous job or acquiring a lock.|No|\`5\`| +|\`defaultRetryInterval\`|A number indicating the time (in milliseconds) to wait before retrying to acquire a lock.|No|\`5\`| +|\`maximumRetryInterval\`|A number indicating the maximum time (in milliseconds) to wait before retrying to acquire a lock.|No|\`200\`| + +*** + +## Test out the Module + +To test out the Redis Locking Module Provider, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message logged in the terminal: + +```bash +info: Connection to Redis in "locking-redis" provider established +``` + +This message indicates that the Redis Locking Module Provider has successfully connected to the Redis server. + +If you set the `is_default` flag to `true` in the provider options or you only registered the Redis Locking Module Provider, the Locking Module will use it by default for all locking operations. + +*** + +## Use Provider with Locking Module + +The Redis Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: + +```ts title="medusa-config.ts" highlights={defaultHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-redis", + id: "locking-redis", + is_default: true, + options: { + // ... + }, + }, + ], + }, + }, + ], +}) +``` + +If you use the Locking Module in your customizations, the Redis Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-redis` to the Locking Module's service methods. + +For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const lockingModuleService = container.resolve( + Modules.LOCKING + ) + + await lockingModuleService.acquire("prod_123", { + provider: "lp_locking-redis", + }) + } +) +``` + + +# PostgreSQL Locking Module Provider + +The PostgreSQL Locking Module Provider uses PostgreSQL's advisory locks to control and manage locks across multiple instances of Medusa. Advisory locks are lightweight locks that do not interfere with other database transactions. By using PostgreSQL's advisory locks, Medusa can create distributed locks directly through the database. + +The provider uses the existing PostgreSQL database in your application to manage locks, so you don't need to set up a separate database or service to manage locks. + +While this provider is suitable for production environments, it's recommended to use the [Redis Locking Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/locking/redis/index.html.md) if possible. + +*** + +## Register the PostgreSQL Locking Module Provider + +To register the PostgreSQL Locking Module Provider, add it to the list of providers of the Locking Module in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-postgres", + id: "locking-postgres", + // set this if you want this provider to be used by default + // and you have other Locking Module Providers registered. + is_default: true, + }, + ], + }, + }, + ], +}) +``` + +### Run Migrations + +The PostgreSQL Locking Module Provider requires a new `locking` table in the database to store the locks. So, you must run the migrations after registering the provider: + +```bash +npx medusa db:migrate +``` + +This will run the migration in the PostgreSQL Locking Module Provider and create the necessary table in the database. + +*** + +## Use Provider with Locking Module + +The PostgreSQL Locking Module Provider will be the default provider if you don't register any other providers, or if you set the `is_default` flag to `true`: + +```ts title="medusa-config.ts" highlights={defaultHighlights} +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/locking", + options: { + providers: [ + { + resolve: "@medusajs/medusa/locking-postgres", + id: "locking-postgres", + is_default: true, + }, + ], + }, + }, + ], +}) +``` + +If you use the Locking Module in your customizations, the PostgreSQL Locking Module Provider will be used by default in this case. You can also explicitly use this provider by passing its identifier `lp_locking-postgres` to the Locking Module's service methods. + +For example, when using the `acquire` method in a [workflow step](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md): + +```ts +import { Modules } from "@medusajs/framework/utils" +import { createStep } from "@medusajs/framework/workflows-sdk" + +const step1 = createStep( + "step-1", + async ({}, { container }) => { + const lockingModuleService = container.resolve( + Modules.LOCKING + ) + + await lockingModuleService.acquire("prod_123", { + provider: "lp_locking-postgres", + }) + } +) +``` + + +# In-Memory Workflow Engine Module + +The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions. + +This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. + +For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md). + +*** + +## Register the In-Memory Workflow Engine Module + +The In-Memory Workflow Engine Module is registered by default in your application. + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/workflow-engine-inmemory", + }, + ], +}) +``` + + +# Redis Workflow Engine Module + +The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module. + +*** + +## Register the Redis Workflow Engine Module + +### Prerequisites + +- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) + +Add the module into the `modules` property of the exported object in `medusa-config.ts`: + +```ts title="medusa-config.ts" highlights={highlights} +import { Modules } from "@medusajs/framework/utils" + +// ... + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/workflow-engine-redis", + options: { + redis: { + url: process.env.WE_REDIS_URL, + }, + }, + }, + ], +}) +``` + +### Environment Variables + +Make sure to add the following environment variables: + +```bash +WE_REDIS_URL= +``` + +### Redis Workflow Engine Module Options + +|Option|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |-| +|\`options\`|An object of Redis options. Refer to the |No|-| +|\`queueName\`|The name of the queue used to keep track of retries and timeouts.|No|\`medusa-workflows\`| +|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |-| + +## Test the Module + +To test the module, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +You'll see the following message in the terminal's logs: + +```bash noCopy noReport +Connection to Redis in module 'workflow-engine-redis' established +``` + + # Redis Event Module The Redis Event Module uses Redis to implement Medusa's pub/sub events system. @@ -32878,176 +33273,6 @@ Connection to Redis in module 'event-redis' established ``` -# How to Create a Cache Module - -In this guide, you’ll learn how to create a Cache Module. - -## 1. Create Module Directory - -Start by creating a new directory for your module. For example, `src/modules/my-cache`. - -*** - -## 2. Create the Cache Service - -Create the file `src/modules/my-cache/service.ts` that holds the implementation of the cache service. - -The Cache Module's main service must implement the `ICacheService` interface imported from `@medusajs/framework/types`: - -```ts title="src/modules/my-cache/service.ts" -import { ICacheService } from "@medusajs/framework/types" - -class MyCacheService implements ICacheService { - get(key: string): Promise { - throw new Error("Method not implemented.") - } - set(key: string, data: unknown, ttl?: number): Promise { - throw new Error("Method not implemented.") - } - invalidate(key: string): Promise { - throw new Error("Method not implemented.") - } -} - -export default MyCacheService -``` - -The service implements the required methods based on the desired caching mechanism. - -### Implement get Method - -The `get` method retrieves the value of a cached item based on its key. - -The method accepts a string as a first parameter, which is the key in the cache. It either returns the cached item or `null` if it doesn’t exist. - -For example, to implement this method using Memcached: - -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - // ... - async get(cacheKey: string): Promise { - return new Promise((res, rej) => { - this.memcached.get(cacheKey, (err, data) => { - if (err) { - res(null) - } else { - if (data) { - res(JSON.parse(data)) - } else { - res(null) - } - } - }) - }) - } -} -``` - -### Implement set Method - -The `set` method is used to set an item in the cache. It accepts three parameters: - -1. The first parameter is a string indicating the key of the data being added to the cache. This key can be used later to get or invalidate the cached item. -2. The second parameter is the data to be added to the cache. The data can be of any type. -3. The third parameter is optional. It’s a number indicating how long (in seconds) the data should be kept in the cache. - -For example, to implement this method using Memcached: - -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - protected TTL = 60 - // ... - async set( - key: string, - data: Record, - ttl: number = this.TTL // or any value - ): Promise { - return new Promise((res, rej) => - this.memcached.set( - key, JSON.stringify(data), ttl, (err) => { - if (err) { - rej(err) - } else { - res() - } - }) - ) - } -} -``` - -### Implement invalidate Method - -The `invalidate` method removes an item from the cache using its key. - -By default, items are removed from the cache when their time-to-live (ttl) expires. The `invalidate` method can be used to remove the item beforehand. - -The method accepts a string as a first parameter, which is the key of the item to invalidate and remove from the cache. - -For example, to implement this method using Memcached: - -```ts title="src/modules/my-cache/service.ts" -class MyCacheService implements ICacheService { - // ... - async invalidate(key: string): Promise { - return new Promise((res, rej) => { - this.memcached.del(key, (err) => { - if (err) { - rej(err) - } else { - res() - } - }) - }) - } -} -``` - -*** - -## 3. Create Module Definition File - -Create the file `src/modules/my-cache/index.ts` with the following content: - -```ts title="src/modules/my-cache/index.ts" -import MyCacheService from "./service" -import { Module } from "@medusajs/framework/utils" - -export default Module("my-cache", { - service: MyCacheService, -}) -``` - -This exports the module's definition, indicating that the `MyCacheService` is the main service of the module. - -*** - -## 4. Use Module - -To use your Cache Module, add it to the `modules` object exported as part of the configurations in `medusa-config.ts`. A Cache Module is added under the `cacheService` key. - -For example: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/my-cache", - options: { - // any options - ttl: 30, - }, - }, - ], -}) -``` - - # How to Use the Workflow Engine Module In this document, you’ll learn about the different methods in the Workflow Engine Module's service and how to use them. @@ -33237,210 +33462,6 @@ await workflowEngineModuleService.unsubscribe({ - subscriberOrId: (\`string\`) The subscriber ID or the subscriber function to unsubscribe from the workflow's events. -# Redis Cache Module - -The Redis Cache Module uses Redis to cache data in your store. In production, it's recommended to use this module. - -*** - -## Register the Redis Cache Module - -### Prerequisites - -- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) - -Add the module into the `modules` property of the exported object in `medusa-config.ts`: - -```ts title="medusa-config.ts" highlights={highlights} -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-redis", - options: { - redisUrl: process.env.CACHE_REDIS_URL, - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the following environment variables: - -```bash -CACHE_REDIS_URL= -``` - -### Redis Cache Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`redisUrl\`|A string indicating the Redis connection URL.|Yes|-| -|\`redisOptions\`|An object of Redis options. Refer to the |No|-| -|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|No|\`30\`| -|\`namespace\`|A string used to prefix all cached keys with |No|\`medusa\`| - -*** - -## Test the Module - -To test the module, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -You'll see the following message in the terminal's logs: - -```bash noCopy noReport -Connection to Redis in module 'cache-redis' established -``` - - -# In-Memory Cache Module - -The In-Memory Cache Module uses a plain JavaScript Map object to store the cached data. This module is used by default in your Medusa application. - -This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. - -For production, it’s recommended to use modules like [Redis Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/redis/index.html.md). - -*** - -## Register the In-Memory Cache Module - -The In-Memory Cache Module is registered by default in your application. - -Add the module into the `modules` property of the exported object in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/cache-inmemory", - options: { - // optional options - }, - }, - ], -}) -``` - -### In-Memory Cache Module Options - -|Option|Description|Default| -|---|---|---|---|---| -|\`ttl\`|The number of seconds an item can live in the cache before it’s removed.|\`30\`| - - -# In-Memory Workflow Engine Module - -The In-Memory Workflow Engine Module uses a plain JavaScript Map object to store the workflow executions. - -This module is helpful for development or when you’re testing out Medusa, but it’s not recommended to be used in production. - -For production, it’s recommended to use modules like [Redis Workflow Engine Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/workflow-engine/redis/index.html.md). - -*** - -## Register the In-Memory Workflow Engine Module - -The In-Memory Workflow Engine Module is registered by default in your application. - -Add the module into the `modules` property of the exported object in `medusa-config.ts`: - -```ts title="medusa-config.ts" -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/workflow-engine-inmemory", - }, - ], -}) -``` - - -# Redis Workflow Engine Module - -The Redis Workflow Engine Module uses Redis to track workflow executions and handle their subscribers. In production, it's recommended to use this module. - -*** - -## Register the Redis Workflow Engine Module - -### Prerequisites - -- [Redis installed and Redis server running](https://redis.io/docs/getting-started/installation/) - -Add the module into the `modules` property of the exported object in `medusa-config.ts`: - -```ts title="medusa-config.ts" highlights={highlights} -import { Modules } from "@medusajs/framework/utils" - -// ... - -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/workflow-engine-redis", - options: { - redis: { - url: process.env.WE_REDIS_URL, - }, - }, - }, - ], -}) -``` - -### Environment Variables - -Make sure to add the following environment variables: - -```bash -WE_REDIS_URL= -``` - -### Redis Workflow Engine Module Options - -|Option|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`url\`|A string indicating the Redis connection URL.|No. If not provided, you must provide the |-| -|\`options\`|An object of Redis options. Refer to the |No|-| -|\`queueName\`|The name of the queue used to keep track of retries and timeouts.|No|\`medusa-workflows\`| -|\`pubsub\`|A connection object having the following properties:|No. If not provided, you must provide the |-| - -## Test the Module - -To test the module, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -You'll see the following message in the terminal's logs: - -```bash noCopy noReport -Connection to Redis in module 'workflow-engine-redis' established -``` - - # Local Notification Module Provider The Local Notification Module Provider simulates sending a notification, but only logs the notification's details in the terminal. This is useful for development. @@ -33490,6 +33511,127 @@ module.exports = defineConfig({ it's important to specify its channels to make sure it's used when a notification for that channel is created.| +# Send Notification with the Notification Module + +In this guide, you'll learn about the different ways to send notifications using the Notification Module. + +## Using the Create Method + +In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method: + +```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { Modules } from "@medusajs/framework/utils" +import { INotificationModuleService } from "@medusajs/framework/types" + +export default async function productCreateHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + const notificationModuleService: INotificationModuleService = + container.resolve(Modules.NOTIFICATION) + + await notificationModuleService.createNotifications({ + to: "user@gmail.com", + channel: "email", + template: "product-created", + data, + }) +} + +export const config: SubscriberConfig = { + event: "product.created", +} +``` + +The `create` method accepts an object or an array of objects having the following properties: + +- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number. +- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification. +- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here. +- data: (\`Record\\`) The data to pass along to the template, if necessary. + +For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md). + +*** + +## Using the sendNotifcationStep + +If you want to send a notification as part of a workflow, You can use the [sendNotifcationStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) 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](https://docs.medusajs.com/resources/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) 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", +} +``` + + # SendGrid Notification Module Provider The SendGrid Notification Module Provider integrates [SendGrid](https://sendgrid.com) to send emails to users and customers. @@ -33706,246 +33848,280 @@ export const config: SubscriberConfig = { This subscriber will run every time a product is created, and it will execute the `sendEmailWorkflow` to send an email using SendGrid. -# Send Notification with the Notification Module - -In this guide, you'll learn about the different ways to send notifications using the Notification Module. - -## Using the Create Method - -In your resource, such as a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md), resolve the Notification Module's main service and use its `create` method: - -```ts title="src/subscribers/product-created.ts" highlights={highlights} collapsibleLines="1-7" expandButtonLabel="Show Imports" -import type { - SubscriberArgs, - SubscriberConfig, -} from "@medusajs/framework" -import { Modules } from "@medusajs/framework/utils" -import { INotificationModuleService } from "@medusajs/framework/types" - -export default async function productCreateHandler({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - const notificationModuleService: INotificationModuleService = - container.resolve(Modules.NOTIFICATION) - - await notificationModuleService.createNotifications({ - to: "user@gmail.com", - channel: "email", - template: "product-created", - data, - }) -} - -export const config: SubscriberConfig = { - event: "product.created", -} -``` - -The `create` method accepts an object or an array of objects having the following properties: - -- to: (\`string\`) The destination to send the notification to. When sending an email, it'll be the email address. When sending an SMS, it'll be the phone number. -- channel: (\`string\`) The channel to send the notification through. For example, \`email\` or \`sms\`. The module provider defined for that channel will be used to send the notification. -- template: (\`string\`) The ID of the template used for the notification. This is useful for providers like SendGrid, where you define templates within SendGrid and use their IDs here. -- data: (\`Record\\`) The data to pass along to the template, if necessary. - -For a full list of properties accepted, refer to [this guide](https://docs.medusajs.com/references/notification-provider-module#create/index.html.md). - -*** - -## Using the sendNotifcationStep - -If you want to send a notification as part of a workflow, You can use the [sendNotifcationStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) 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](https://docs.medusajs.com/resources/references/medusa-workflows/steps/sendNotificationsStep/index.html.md) 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", -} -``` - - ## Workflows -- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) -- [revokeApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/revokeApiKeysWorkflow/index.html.md) -- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/index.html.md) -- [updateApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateApiKeysWorkflow/index.html.md) -- [deleteApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteApiKeysWorkflow/index.html.md) - [createCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAccountWorkflow/index.html.md) - [createCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerAddressesWorkflow/index.html.md) -- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/index.html.md) - [deleteCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerAddressesWorkflow/index.html.md) - [deleteCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomersWorkflow/index.html.md) - [removeCustomerAccountWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeCustomerAccountWorkflow/index.html.md) -- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) - [updateCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomersWorkflow/index.html.md) +- [updateCustomerAddressesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerAddressesWorkflow/index.html.md) +- [createCustomersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomersWorkflow/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) +- [createApiKeysWorkflow](https://docs.medusajs.com/references/medusa-workflows/createApiKeysWorkflow/index.html.md) +- [linkSalesChannelsToApiKeyWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToApiKeyWorkflow/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) +- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) +- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) +- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) +- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) +- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) +- [listShippingOptionsForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWorkflow/index.html.md) +- [refreshCartShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartShippingMethodsWorkflow/index.html.md) +- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/index.html.md) +- [refundPaymentAndRecreatePaymentSessionWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentAndRecreatePaymentSessionWorkflow/index.html.md) +- [refreshCartItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshCartItemsWorkflow/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) +- [transferCartCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/transferCartCustomerWorkflow/index.html.md) +- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) +- [validateExistingPaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/validateExistingPaymentCollectionStep/index.html.md) +- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) - [createCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCustomerGroupsWorkflow/index.html.md) - [deleteCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCustomerGroupsWorkflow/index.html.md) - [linkCustomerGroupsToCustomerWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomerGroupsToCustomerWorkflow/index.html.md) -- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) - [linkCustomersToCustomerGroupWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkCustomersToCustomerGroupWorkflow/index.html.md) -- [generateResetPasswordTokenWorkflow](https://docs.medusajs.com/references/medusa-workflows/generateResetPasswordTokenWorkflow/index.html.md) -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md) -- [completeCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeCartWorkflow/index.html.md) -- [confirmVariantInventoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmVariantInventoryWorkflow/index.html.md) -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md) -- [createCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartCreditLinesWorkflow/index.html.md) -- [deleteCartCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCartCreditLinesWorkflow/index.html.md) -- [createPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentCollectionForCartWorkflow/index.html.md) -- [listShippingOptionsForCartWithPricingWorkflow](https://docs.medusajs.com/references/medusa-workflows/listShippingOptionsForCartWithPricingWorkflow/index.html.md) -- [addToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addToCartWorkflow/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) -- [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) -- [updateLineItemInCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLineItemInCartWorkflow/index.html.md) -- [refreshPaymentCollectionForCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/refreshPaymentCollectionForCartWorkflow/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) -- [updateTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxLinesWorkflow/index.html.md) -- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/index.html.md) -- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) -- [dismissLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissLinksWorkflow/index.html.md) -- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/index.html.md) -- [createLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLinksWorkflow/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) -- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) +- [updateCustomerGroupsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCustomerGroupsWorkflow/index.html.md) - [addDraftOrderPromotionWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderPromotionWorkflow/index.html.md) - [addDraftOrderItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderItemsWorkflow/index.html.md) -- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/index.html.md) +- [addDraftOrderShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addDraftOrderShippingMethodsWorkflow/index.html.md) - [cancelDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelDraftOrderEditWorkflow/index.html.md) - [confirmDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmDraftOrderEditWorkflow/index.html.md) -- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) -- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/index.html.md) -- [removeDraftOrderPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeDraftOrderPromotionsWorkflow/index.html.md) +- [beginDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginDraftOrderEditWorkflow/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) -- [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) +- [convertDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderWorkflow/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) +- [convertDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/convertDraftOrderStep/index.html.md) +- [requestDraftOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestDraftOrderEditWorkflow/index.html.md) +- [updateDraftOrderShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderShippingMethodWorkflow/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) +- [updateDraftOrderActionShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderActionShippingMethodWorkflow/index.html.md) - [updateDraftOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateDraftOrderWorkflow/index.html.md) +- [deleteFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteFilesWorkflow/index.html.md) +- [uploadFilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/uploadFilesWorkflow/index.html.md) - [batchShippingOptionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchShippingOptionRulesWorkflow/index.html.md) - [calculateShippingOptionsPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/calculateShippingOptionsPricesWorkflow/index.html.md) - [cancelFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelFulfillmentWorkflow/index.html.md) -- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) -- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) -- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) - [createReturnFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnFulfillmentWorkflow/index.html.md) +- [createShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShipmentWorkflow/index.html.md) +- [createShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingOptionsWorkflow/index.html.md) +- [createFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentWorkflow/index.html.md) - [createServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createServiceZonesWorkflow/index.html.md) - [createShippingProfilesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createShippingProfilesWorkflow/index.html.md) -- [deleteServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteServiceZonesWorkflow/index.html.md) -- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/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) - [markFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markFulfillmentAsDeliveredWorkflow/index.html.md) - [updateServiceZonesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateServiceZonesWorkflow/index.html.md) - [updateFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateFulfillmentWorkflow/index.html.md) - [updateShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateShippingOptionsWorkflow/index.html.md) +- [deleteShippingOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteShippingOptionsWorkflow/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) -- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) - [batchInventoryItemLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchInventoryItemLevelsWorkflow/index.html.md) - [createInventoryItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryItemsWorkflow/index.html.md) +- [bulkCreateDeleteLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/bulkCreateDeleteLevelsWorkflow/index.html.md) - [createInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInventoryLevelsWorkflow/index.html.md) -- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) - [deleteInventoryItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryItemWorkflow/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) - [updateInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateInventoryLevelsWorkflow/index.html.md) -- [createInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createInvitesWorkflow/index.html.md) -- [acceptInviteWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptInviteWorkflow/index.html.md) -- [deleteInvitesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInvitesWorkflow/index.html.md) +- [deleteInventoryLevelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteInventoryLevelsWorkflow/index.html.md) +- [validateInventoryLevelsDelete](https://docs.medusajs.com/references/medusa-workflows/validateInventoryLevelsDelete/index.html.md) - [deleteLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteLineItemsWorkflow/index.html.md) +- [createDefaultsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createDefaultsWorkflow/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) -- [refundPaymentWorkflow](https://docs.medusajs.com/references/medusa-workflows/refundPaymentWorkflow/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) - [validatePaymentsRefundStep](https://docs.medusajs.com/references/medusa-workflows/validatePaymentsRefundStep/index.html.md) -- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/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) -- [createPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListPricesWorkflow/index.html.md) -- [createPriceListsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPriceListsWorkflow/index.html.md) -- [batchPriceListPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchPriceListPricesWorkflow/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) -- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/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) +- [validateRefundStep](https://docs.medusajs.com/references/medusa-workflows/validateRefundStep/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) +- [batchLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinksWorkflow/index.html.md) +- [updateLinksWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateLinksWorkflow/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) +- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/index.html.md) +- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) +- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) +- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) +- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) +- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) +- [beginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditValidationStep/index.html.md) +- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) +- [cancelBeginOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimWorkflow/index.html.md) +- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/index.html.md) +- [beginOrderEditOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginOrderEditOrderWorkflow/index.html.md) +- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) +- [cancelBeginOrderEditWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditWorkflow/index.html.md) +- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) +- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) +- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) +- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) +- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) +- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) +- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) +- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) +- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) +- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/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) +- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) +- [cancelReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnReceiveWorkflow/index.html.md) +- [cancelReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnRequestWorkflow/index.html.md) +- [cancelReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelReturnWorkflow/index.html.md) +- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/index.html.md) +- [cancelValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelValidateOrder/index.html.md) +- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) +- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/index.html.md) +- [confirmClaimRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestWorkflow/index.html.md) +- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) +- [completeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/completeOrderWorkflow/index.html.md) +- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) +- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) +- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) +- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) +- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/index.html.md) +- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) +- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) +- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) +- [createClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodValidationStep/index.html.md) +- [createClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createClaimShippingMethodWorkflow/index.html.md) +- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) +- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/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) +- [createOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeActionsWorkflow/index.html.md) +- [createOrUpdateOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrUpdateOrderPaymentCollectionWorkflow/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) +- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/index.html.md) +- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) +- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/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) +- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/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) +- [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) +- [deleteOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeActionsWorkflow/index.html.md) +- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/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) +- [dismissItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestValidationStep/index.html.md) +- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) +- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) +- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) +- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) +- [fetchShippingOptionForOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/fetchShippingOptionForOrderWorkflow/index.html.md) +- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/index.html.md) +- [getOrdersListWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrdersListWorkflow/index.html.md) +- [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) +- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) +- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/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) +- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) +- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) +- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) +- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) +- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) +- [orderExchangeAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeAddNewItemWorkflow/index.html.md) +- [orderExchangeRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderExchangeRequestItemReturnWorkflow/index.html.md) +- [orderFulfillmentDeliverablilityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderFulfillmentDeliverablilityValidationStep/index.html.md) +- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/index.html.md) +- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) +- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) +- [removeClaimAddItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimAddItemActionValidationStep/index.html.md) +- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/index.html.md) +- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) +- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) +- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) +- [removeClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodWorkflow/index.html.md) +- [removeItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemClaimActionWorkflow/index.html.md) +- [removeExchangeItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeExchangeItemActionValidationStep/index.html.md) +- [removeExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeExchangeShippingMethodWorkflow/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) +- [removeItemOrderEditActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemOrderEditActionWorkflow/index.html.md) +- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) +- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/index.html.md) +- [removeItemReceiveReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionWorkflow/index.html.md) +- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) +- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) +- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) +- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) +- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) +- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) +- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) +- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) +- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) +- [requestOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferValidationStep/index.html.md) +- [requestOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderTransferWorkflow/index.html.md) +- [requestOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestWorkflow/index.html.md) +- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) +- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) +- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) +- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) +- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) +- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) +- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) +- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) +- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) +- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) +- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) +- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) +- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) +- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) +- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) +- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/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) +- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) +- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) +- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) +- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/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) +- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/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) +- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) +- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) +- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) +- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) +- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) +- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) +- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) - [batchLinkProductsToCategoryWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchLinkProductsToCategoryWorkflow/index.html.md) -- [batchProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/batchProductsWorkflow/index.html.md) - [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) - [createProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductOptionsWorkflow/index.html.md) - [createProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createProductTagsWorkflow/index.html.md) @@ -33961,501 +34137,346 @@ export const config: SubscriberConfig = { - [exportProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/exportProductsWorkflow/index.html.md) - [importProductsAsChunksWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsAsChunksWorkflow/index.html.md) - [importProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/importProductsWorkflow/index.html.md) -- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) -- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateCollectionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCollectionsWorkflow/index.html.md) +- [updateProductTagsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTagsWorkflow/index.html.md) - [updateProductTypesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductTypesWorkflow/index.html.md) -- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) -- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/index.html.md) +- [updateProductOptionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductOptionsWorkflow/index.html.md) - [updateProductVariantsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductVariantsWorkflow/index.html.md) +- [updateProductsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductsWorkflow/index.html.md) - [validateProductInputStep](https://docs.medusajs.com/references/medusa-workflows/validateProductInputStep/index.html.md) -- [acceptOrderTransferValidationStep](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferValidationStep/index.html.md) -- [acceptOrderTransferWorkflow](https://docs.medusajs.com/references/medusa-workflows/acceptOrderTransferWorkflow/index.html.md) -- [addOrderLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrderLineItemsWorkflow/index.html.md) -- [archiveOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/archiveOrderWorkflow/index.html.md) -- [beginClaimOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderValidationStep/index.html.md) -- [beginClaimOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginClaimOrderWorkflow/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) -- [beginExchangeOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginExchangeOrderWorkflow/index.html.md) -- [beginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginOrderExchangeValidationStep/index.html.md) -- [beginReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnValidationStep/index.html.md) -- [beginReceiveReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReceiveReturnWorkflow/index.html.md) -- [beginReturnOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderValidationStep/index.html.md) -- [beginReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/beginReturnOrderWorkflow/index.html.md) -- [cancelBeginOrderClaimValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderClaimValidationStep/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) -- [cancelBeginOrderEditValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderEditValidationStep/index.html.md) -- [cancelBeginOrderExchangeValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeValidationStep/index.html.md) -- [cancelBeginOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelBeginOrderExchangeWorkflow/index.html.md) -- [cancelClaimValidateOrderStep](https://docs.medusajs.com/references/medusa-workflows/cancelClaimValidateOrderStep/index.html.md) -- [cancelExchangeValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelExchangeValidateOrder/index.html.md) -- [cancelOrderClaimWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderClaimWorkflow/index.html.md) -- [cancelOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderChangeWorkflow/index.html.md) -- [cancelOrderFulfillmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentWorkflow/index.html.md) -- [cancelOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderTransferRequestWorkflow/index.html.md) -- [cancelOrderFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelOrderFulfillmentValidateOrder/index.html.md) -- [cancelOrderExchangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderExchangeWorkflow/index.html.md) -- [cancelOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/cancelOrderWorkflow/index.html.md) -- [cancelReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelReceiveReturnValidationStep/index.html.md) -- [cancelRequestReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelRequestReturnValidationStep/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) -- [cancelReturnValidateOrder](https://docs.medusajs.com/references/medusa-workflows/cancelReturnValidateOrder/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) -- [cancelTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/cancelTransferOrderRequestValidationStep/index.html.md) -- [confirmClaimRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmClaimRequestValidationStep/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) -- [confirmOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestValidationStep/index.html.md) -- [confirmExchangeRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestWorkflow/index.html.md) -- [confirmExchangeRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmExchangeRequestValidationStep/index.html.md) -- [confirmReceiveReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReceiveReturnValidationStep/index.html.md) -- [confirmOrderEditRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmOrderEditRequestWorkflow/index.html.md) -- [confirmReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestValidationStep/index.html.md) -- [confirmReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnRequestWorkflow/index.html.md) -- [confirmReturnReceiveWorkflow](https://docs.medusajs.com/references/medusa-workflows/confirmReturnReceiveWorkflow/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) -- [createAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createAndCompleteReturnOrderWorkflow/index.html.md) -- [createExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodWorkflow/index.html.md) -- [createExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createExchangeShippingMethodValidationStep/index.html.md) -- [createCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/createCompleteReturnValidationStep/index.html.md) -- [createFulfillmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createFulfillmentValidateOrder/index.html.md) -- [createOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderChangeWorkflow/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) -- [createOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createOrderEditShippingMethodValidationStep/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) -- [createOrderCreditLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderCreditLinesWorkflow/index.html.md) -- [createOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderWorkflow/index.html.md) -- [createOrderShipmentWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderShipmentWorkflow/index.html.md) -- [createOrderPaymentCollectionWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrderPaymentCollectionWorkflow/index.html.md) -- [createOrdersWorkflow](https://docs.medusajs.com/references/medusa-workflows/createOrdersWorkflow/index.html.md) -- [createReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodValidationStep/index.html.md) -- [declineOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderChangeWorkflow/index.html.md) -- [createShipmentValidateOrder](https://docs.medusajs.com/references/medusa-workflows/createShipmentValidateOrder/index.html.md) -- [declineOrderTransferRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/declineOrderTransferRequestWorkflow/index.html.md) -- [createReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnShippingMethodWorkflow/index.html.md) -- [declineTransferOrderRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/declineTransferOrderRequestValidationStep/index.html.md) -- [deleteOrderChangeWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteOrderChangeWorkflow/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) -- [dismissItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/dismissItemReturnRequestWorkflow/index.html.md) -- [exchangeAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeAddNewItemValidationStep/index.html.md) -- [exchangeRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/exchangeRequestItemReturnValidationStep/index.html.md) -- [getOrderDetailWorkflow](https://docs.medusajs.com/references/medusa-workflows/getOrderDetailWorkflow/index.html.md) -- [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) -- [maybeRefreshShippingMethodsWorkflow](https://docs.medusajs.com/references/medusa-workflows/maybeRefreshShippingMethodsWorkflow/index.html.md) -- [orderClaimAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimAddNewItemValidationStep/index.html.md) -- [markOrderFulfillmentAsDeliveredWorkflow](https://docs.medusajs.com/references/medusa-workflows/markOrderFulfillmentAsDeliveredWorkflow/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) -- [orderClaimRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnWorkflow/index.html.md) -- [orderClaimRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderClaimRequestItemReturnValidationStep/index.html.md) -- [orderEditAddNewItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemWorkflow/index.html.md) -- [orderEditUpdateItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityValidationStep/index.html.md) -- [orderEditUpdateItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/orderEditUpdateItemQuantityWorkflow/index.html.md) -- [orderEditAddNewItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/orderEditAddNewItemValidationStep/index.html.md) -- [receiveAndCompleteReturnOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveAndCompleteReturnOrderWorkflow/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) -- [receiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestValidationStep/index.html.md) -- [receiveCompleteReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/receiveCompleteReturnValidationStep/index.html.md) -- [receiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/receiveItemReturnRequestWorkflow/index.html.md) -- [removeAddItemClaimActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeAddItemClaimActionWorkflow/index.html.md) -- [removeClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimShippingMethodValidationStep/index.html.md) -- [removeClaimItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeClaimItemActionValidationStep/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) -- [removeItemReceiveReturnActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeItemReceiveReturnActionValidationStep/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) -- [removeItemReturnActionWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeItemReturnActionWorkflow/index.html.md) -- [removeOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodWorkflow/index.html.md) -- [removeOrderEditItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditItemActionValidationStep/index.html.md) -- [removeOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeOrderEditShippingMethodValidationStep/index.html.md) -- [removeReturnItemActionValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnItemActionValidationStep/index.html.md) -- [removeReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodValidationStep/index.html.md) -- [requestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnValidationStep/index.html.md) -- [removeReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/removeReturnShippingMethodWorkflow/index.html.md) -- [requestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/requestItemReturnWorkflow/index.html.md) -- [requestOrderEditRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/requestOrderEditRequestValidationStep/index.html.md) -- [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) -- [throwUnlessStatusIsNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessStatusIsNotPaid/index.html.md) -- [updateClaimAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemValidationStep/index.html.md) -- [throwUnlessPaymentCollectionNotPaid](https://docs.medusajs.com/references/medusa-workflows/throwUnlessPaymentCollectionNotPaid/index.html.md) -- [updateClaimAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimAddItemWorkflow/index.html.md) -- [updateClaimItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemValidationStep/index.html.md) -- [updateClaimShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodValidationStep/index.html.md) -- [updateClaimItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimItemWorkflow/index.html.md) -- [updateClaimShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateClaimShippingMethodWorkflow/index.html.md) -- [updateExchangeAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemWorkflow/index.html.md) -- [updateExchangeAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeAddItemValidationStep/index.html.md) -- [updateExchangeShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodValidationStep/index.html.md) -- [updateExchangeShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateExchangeShippingMethodWorkflow/index.html.md) -- [updateOrderChangeActionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangeActionsWorkflow/index.html.md) -- [updateOrderChangesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderChangesWorkflow/index.html.md) -- [updateOrderEditAddItemValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemValidationStep/index.html.md) -- [updateOrderEditAddItemWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditAddItemWorkflow/index.html.md) -- [updateOrderEditItemQuantityWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityWorkflow/index.html.md) -- [updateOrderEditItemQuantityValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditItemQuantityValidationStep/index.html.md) -- [updateOrderEditShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodValidationStep/index.html.md) -- [updateOrderEditShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderEditShippingMethodWorkflow/index.html.md) -- [updateOrderWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderWorkflow/index.html.md) -- [updateOrderValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateOrderValidationStep/index.html.md) -- [updateOrderTaxLinesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateOrderTaxLinesWorkflow/index.html.md) -- [updateReceiveItemReturnRequestWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestWorkflow/index.html.md) -- [updateRequestItemReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnValidationStep/index.html.md) -- [updateRequestItemReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRequestItemReturnWorkflow/index.html.md) -- [updateReceiveItemReturnRequestValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReceiveItemReturnRequestValidationStep/index.html.md) -- [updateReturnShippingMethodValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodValidationStep/index.html.md) -- [updateReturnShippingMethodWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnShippingMethodWorkflow/index.html.md) -- [updateReturnValidationStep](https://docs.medusajs.com/references/medusa-workflows/updateReturnValidationStep/index.html.md) -- [updateReturnWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnWorkflow/index.html.md) -- [validateOrderCreditLinesStep](https://docs.medusajs.com/references/medusa-workflows/validateOrderCreditLinesStep/index.html.md) -- [createRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRefundReasonsWorkflow/index.html.md) -- [deletePaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePaymentSessionsWorkflow/index.html.md) -- [deleteRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRefundReasonsWorkflow/index.html.md) -- [createPaymentSessionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPaymentSessionsWorkflow/index.html.md) -- [updateRefundReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRefundReasonsWorkflow/index.html.md) +- [upsertVariantPricesWorkflow](https://docs.medusajs.com/references/medusa-workflows/upsertVariantPricesWorkflow/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) +- [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) - [updateProductCategoriesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateProductCategoriesWorkflow/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) - [createRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createRegionsWorkflow/index.html.md) -- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/index.html.md) - [deleteRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteRegionsWorkflow/index.html.md) -- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) +- [updateRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateRegionsWorkflow/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) +- [createCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCampaignsWorkflow/index.html.md) +- [addOrRemoveCampaignPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/addOrRemoveCampaignPromotionsWorkflow/index.html.md) - [createPromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createPromotionsWorkflow/index.html.md) - [deletePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionRulesWorkflow/index.html.md) -- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) -- [updatePromotionRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionRulesWorkflow/index.html.md) - [deletePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deletePromotionsWorkflow/index.html.md) +- [deleteCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteCampaignsWorkflow/index.html.md) - [updateCampaignsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateCampaignsWorkflow/index.html.md) - [updatePromotionsStatusWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsStatusWorkflow/index.html.md) +- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/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) - [createReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReservationsWorkflow/index.html.md) -- [updatePromotionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePromotionsWorkflow/index.html.md) - [deleteReservationsByLineItemsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsByLineItemsWorkflow/index.html.md) - [deleteReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReservationsWorkflow/index.html.md) - [updateReservationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReservationsWorkflow/index.html.md) +- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) +- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) +- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) +- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) - [createReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createReturnReasonsWorkflow/index.html.md) - [deleteReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteReturnReasonsWorkflow/index.html.md) - [updateReturnReasonsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateReturnReasonsWorkflow/index.html.md) -- [createSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createSalesChannelsWorkflow/index.html.md) -- [linkProductsToSalesChannelWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkProductsToSalesChannelWorkflow/index.html.md) -- [deleteSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteSalesChannelsWorkflow/index.html.md) -- [updateSalesChannelsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateSalesChannelsWorkflow/index.html.md) -- [validateStepShippingProfileDelete](https://docs.medusajs.com/references/medusa-workflows/validateStepShippingProfileDelete/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) +- [updatePricePreferencesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updatePricePreferencesWorkflow/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) - [createStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createStockLocationsWorkflow/index.html.md) -- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) - [createLocationFulfillmentSetWorkflow](https://docs.medusajs.com/references/medusa-workflows/createLocationFulfillmentSetWorkflow/index.html.md) +- [deleteStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStockLocationsWorkflow/index.html.md) - [linkSalesChannelsToStockLocationWorkflow](https://docs.medusajs.com/references/medusa-workflows/linkSalesChannelsToStockLocationWorkflow/index.html.md) - [updateStockLocationsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateStockLocationsWorkflow/index.html.md) -- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/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) +- [deleteStoresWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteStoresWorkflow/index.html.md) - [createTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRateRulesWorkflow/index.html.md) -- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) +- [createTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRatesWorkflow/index.html.md) - [deleteTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRateRulesWorkflow/index.html.md) -- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) +- [createTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/createTaxRegionsWorkflow/index.html.md) - [deleteTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRatesWorkflow/index.html.md) +- [deleteTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/deleteTaxRegionsWorkflow/index.html.md) - [maybeListTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/maybeListTaxRateRuleIdsStep/index.html.md) -- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) - [updateTaxRatesWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRatesWorkflow/index.html.md) +- [setTaxRateRulesWorkflow](https://docs.medusajs.com/references/medusa-workflows/setTaxRateRulesWorkflow/index.html.md) - [updateTaxRegionsWorkflow](https://docs.medusajs.com/references/medusa-workflows/updateTaxRegionsWorkflow/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) -- [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) ## Steps -- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) - [createApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/createApiKeysStep/index.html.md) - [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) -- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) -- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) -- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) -- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) -- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) -- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) -- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) -- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) -- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) -- [findSalesChannelStep](https://docs.medusajs.com/references/medusa-workflows/steps/findSalesChannelStep/index.html.md) -- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) -- [getLineItemActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getLineItemActionsStep/index.html.md) -- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) -- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) -- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) -- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) -- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) -- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) -- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) -- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) -- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) -- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) -- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) -- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) -- [updateShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingMethodsStep/index.html.md) -- [validateAndReturnShippingMethodsDataStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateAndReturnShippingMethodsDataStep/index.html.md) -- [validateCartPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartPaymentsStep/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) -- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) -- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) -- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/index.html.md) +- [revokeApiKeysStep](https://docs.medusajs.com/references/medusa-workflows/steps/revokeApiKeysStep/index.html.md) - [createRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRemoteLinkStep/index.html.md) -- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) +- [createEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createEntitiesStep/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) +- [emitEventStep](https://docs.medusajs.com/references/medusa-workflows/steps/emitEventStep/index.html.md) - [deleteEntitiesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteEntitiesStep/index.html.md) -- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/index.html.md) - [removeRemoteLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRemoteLinkStep/index.html.md) -- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/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) -- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/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) -- [deleteCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomersStep/index.html.md) -- [createCustomersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCustomersStep/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) -- [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) +- [validatePresenceOfStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePresenceOfStep/index.html.md) +- [useQueryGraphStep](https://docs.medusajs.com/references/medusa-workflows/steps/useQueryGraphStep/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) -- [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) - [linkCustomersToCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/linkCustomersToCustomerGroupStep/index.html.md) -- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/index.html.md) -- [deleteFilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFilesStep/index.html.md) +- [deleteCustomerGroupStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCustomerGroupStep/index.html.md) +- [updateCustomerGroupsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerGroupsStep/index.html.md) +- [validateDraftOrderStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDraftOrderStep/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) +- [setAuthAppMetadataStep](https://docs.medusajs.com/references/medusa-workflows/steps/setAuthAppMetadataStep/index.html.md) +- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) +- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) +- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) +- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) +- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) +- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/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) +- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) +- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/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) +- [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) +- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) +- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) +- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) +- [updateFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateFulfillmentStep/index.html.md) +- [updateShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingProfilesStep/index.html.md) +- [upsertShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/upsertShippingOptionsStep/index.html.md) +- [validateShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShipmentStep/index.html.md) +- [validateShippingOptionPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingOptionPricesStep/index.html.md) +- [createDefaultStoreStep](https://docs.medusajs.com/references/medusa-workflows/steps/createDefaultStoreStep/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) +- [attachInventoryItemToVariants](https://docs.medusajs.com/references/medusa-workflows/steps/attachInventoryItemToVariants/index.html.md) - [updateInventoryItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryItemsStep/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) -- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) +- [createInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInventoryLevelsStep/index.html.md) - [updateInventoryLevelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateInventoryLevelsStep/index.html.md) -- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) +- [validateInventoryItemsForCreate](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryItemsForCreate/index.html.md) - [validateInventoryDeleteStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryDeleteStep/index.html.md) -- [buildPriceSet](https://docs.medusajs.com/references/medusa-workflows/steps/buildPriceSet/index.html.md) -- [calculateShippingOptionsPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/calculateShippingOptionsPricesStep/index.html.md) -- [cancelFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelFulfillmentStep/index.html.md) -- [createReturnFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnFulfillmentStep/index.html.md) -- [createFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentStep/index.html.md) -- [createFulfillmentSets](https://docs.medusajs.com/references/medusa-workflows/steps/createFulfillmentSets/index.html.md) -- [createShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionRulesStep/index.html.md) -- [createServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createServiceZonesStep/index.html.md) -- [createShippingProfilesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingProfilesStep/index.html.md) -- [createShippingOptionsPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingOptionsPriceSetsStep/index.html.md) -- [deleteFulfillmentSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteFulfillmentSetsStep/index.html.md) -- [deleteServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteServiceZonesStep/index.html.md) -- [deleteShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionRulesStep/index.html.md) -- [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) -- [deleteShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteShippingOptionsStep/index.html.md) -- [updateServiceZonesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateServiceZonesStep/index.html.md) -- [updateShippingOptionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateShippingOptionRulesStep/index.html.md) -- [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) -- [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) -- [createInviteStep](https://docs.medusajs.com/references/medusa-workflows/steps/createInviteStep/index.html.md) +- [validateInventoryLocationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateInventoryLocationsStep/index.html.md) - [deleteInvitesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteInvitesStep/index.html.md) - [refreshInviteTokensStep](https://docs.medusajs.com/references/medusa-workflows/steps/refreshInviteTokensStep/index.html.md) +- [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) +- [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) - [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) +- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) +- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) +- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/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) - [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) -- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/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) +- [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) +- [archiveOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/archiveOrdersStep/index.html.md) +- [cancelOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderFulfillmentStep/index.html.md) - [cancelOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrdersStep/index.html.md) -- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) -- [createCompleteReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCompleteReturnStep/index.html.md) - [cancelOrderReturnStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelOrderReturnStep/index.html.md) -- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) +- [completeOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/completeOrdersStep/index.html.md) - [createOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderChangeStep/index.html.md) +- [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) +- [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) +- [createOrderClaimItemsFromActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderClaimItemsFromActionsStep/index.html.md) - [createReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReturnsStep/index.html.md) -- [createOrderExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrderExchangesStep/index.html.md) - [createOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createOrdersStep/index.html.md) - [declineOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/declineOrderChangeStep/index.html.md) -- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) - [deleteExchangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteExchangesStep/index.html.md) -- [deleteOrderShippingMethods](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderShippingMethods/index.html.md) +- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) - [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) -- [deleteOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteOrderChangesStep/index.html.md) -- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/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) -- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) +- [previewOrderChangeStep](https://docs.medusajs.com/references/medusa-workflows/steps/previewOrderChangeStep/index.html.md) +- [deleteClaimsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteClaimsStep/index.html.md) - [registerOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderChangesStep/index.html.md) +- [registerOrderDeliveryStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderDeliveryStep/index.html.md) - [registerOrderFulfillmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderFulfillmentStep/index.html.md) -- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) - [setOrderTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setOrderTaxLinesForItemsStep/index.html.md) - [updateOrderChangeActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangeActionsStep/index.html.md) +- [registerOrderShipmentStep](https://docs.medusajs.com/references/medusa-workflows/steps/registerOrderShipmentStep/index.html.md) - [updateOrderChangesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderChangesStep/index.html.md) -- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/index.html.md) -- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateOrderShippingMethodsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrderShippingMethodsStep/index.html.md) +- [updateOrdersStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateOrdersStep/index.html.md) - [updateReturnsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnsStep/index.html.md) -- [authorizePaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/authorizePaymentSessionStep/index.html.md) -- [cancelPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/cancelPaymentStep/index.html.md) -- [refundPaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentStep/index.html.md) -- [capturePaymentStep](https://docs.medusajs.com/references/medusa-workflows/steps/capturePaymentStep/index.html.md) -- [refundPaymentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/refundPaymentsStep/index.html.md) -- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) -- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) -- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) -- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) -- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) -- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) -- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) -- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) -- [deletePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePriceListsStep/index.html.md) -- [createPriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceListsStep/index.html.md) +- [updateReturnItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReturnItemsStep/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) - [updatePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListPricesStep/index.html.md) - [removePriceListPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/removePriceListPricesStep/index.html.md) -- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) - [updatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceListsStep/index.html.md) - [validatePriceListsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validatePriceListsStep/index.html.md) +- [validateVariantPriceLinksStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPriceLinksStep/index.html.md) +- [createPaymentSessionStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentSessionStep/index.html.md) +- [createPaymentAccountHolderStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentAccountHolderStep/index.html.md) +- [createRefundReasonStep](https://docs.medusajs.com/references/medusa-workflows/steps/createRefundReasonStep/index.html.md) +- [deletePaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePaymentSessionsStep/index.html.md) +- [deleteRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteRefundReasonsStep/index.html.md) +- [updateRefundReasonsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateRefundReasonsStep/index.html.md) +- [validateDeletedPaymentSessionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateDeletedPaymentSessionsStep/index.html.md) +- [updatePaymentCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePaymentCollectionStep/index.html.md) +- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) +- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) +- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) +- [updatePricePreferencesAsArrayStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePricePreferencesAsArrayStep/index.html.md) +- [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) +- [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) - [batchLinkProductsToCategoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCategoryStep/index.html.md) -- [createProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductOptionsStep/index.html.md) -- [createCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCollectionsStep/index.html.md) - [batchLinkProductsToCollectionStep](https://docs.medusajs.com/references/medusa-workflows/steps/batchLinkProductsToCollectionStep/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) -- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/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) - [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) +- [createProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductVariantsStep/index.html.md) +- [createProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductsStep/index.html.md) - [createVariantPricingLinkStep](https://docs.medusajs.com/references/medusa-workflows/steps/createVariantPricingLinkStep/index.html.md) - [deleteCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteCollectionsStep/index.html.md) - [deleteProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductOptionsStep/index.html.md) -- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [deleteProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTagsStep/index.html.md) +- [deleteProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductTypesStep/index.html.md) - [deleteProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductVariantsStep/index.html.md) - [deleteProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductsStep/index.html.md) - [generateProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/generateProductCsvStep/index.html.md) -- [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) +- [getAllProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getAllProductsStep/index.html.md) - [getVariantAvailabilityStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantAvailabilityStep/index.html.md) -- [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) - [normalizeCsvToChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvToChunksStep/index.html.md) +- [normalizeCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/normalizeCsvStep/index.html.md) - [processImportChunksStep](https://docs.medusajs.com/references/medusa-workflows/steps/processImportChunksStep/index.html.md) - [updateCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCollectionsStep/index.html.md) -- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductOptionsStep/index.html.md) +- [updateProductTagsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTagsStep/index.html.md) - [updateProductTypesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductTypesStep/index.html.md) - [updateProductVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductVariantsStep/index.html.md) - [updateProductsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductsStep/index.html.md) - [waitConfirmationProductImportStep](https://docs.medusajs.com/references/medusa-workflows/steps/waitConfirmationProductImportStep/index.html.md) -- [createProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createProductCategoriesStep/index.html.md) -- [deleteProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteProductCategoriesStep/index.html.md) -- [updateProductCategoriesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateProductCategoriesStep/index.html.md) -- [createPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPriceSetsStep/index.html.md) -- [createPricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPricePreferencesStep/index.html.md) -- [deletePricePreferencesStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePricePreferencesStep/index.html.md) -- [updatePriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePriceSetsStep/index.html.md) -- [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) -- [addRulesToPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addRulesToPromotionsStep/index.html.md) -- [addCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/addCampaignPromotionsStep/index.html.md) -- [createCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCampaignsStep/index.html.md) -- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/index.html.md) -- [removeCampaignPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeCampaignPromotionsStep/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) -- [removeRulesFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeRulesFromPromotionsStep/index.html.md) -- [updateCampaignsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCampaignsStep/index.html.md) -- [updatePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionsStep/index.html.md) -- [updatePromotionRulesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updatePromotionRulesStep/index.html.md) -- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) -- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) -- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) -- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) - [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) -- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/index.html.md) +- [createReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createReservationsStep/index.html.md) +- [deleteReservationsByLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsByLineItemsStep/index.html.md) +- [deleteReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteReservationsStep/index.html.md) +- [updateReservationsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateReservationsStep/index.html.md) +- [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) +- [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) +- [deletePromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deletePromotionsStep/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) +- [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) - [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) -- [createSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createSalesChannelsStep/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) - [deleteSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteSalesChannelsStep/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) -- [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) - [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) +- [parseProductCsvStep](https://docs.medusajs.com/references/medusa-workflows/steps/parseProductCsvStep/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) - [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) -- [createTaxRegionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createTaxRegionsStep/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) - [listTaxRateRuleIdsStep](https://docs.medusajs.com/references/medusa-workflows/steps/listTaxRateRuleIdsStep/index.html.md) -- [getItemTaxLinesStep](https://docs.medusajs.com/references/medusa-workflows/steps/getItemTaxLinesStep/index.html.md) -- [createUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/createUsersStep/index.html.md) - [updateTaxRatesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateTaxRatesStep/index.html.md) -- [deleteUsersStep](https://docs.medusajs.com/references/medusa-workflows/steps/deleteUsersStep/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) +- [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) +- [associateLocationsWithSalesChannelsStep](https://docs.medusajs.com/references/medusa-workflows/steps/associateLocationsWithSalesChannelsStep/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) +- [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) +- [updateCustomerAddressesStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCustomerAddressesStep/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) +- [confirmInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/confirmInventoryStep/index.html.md) +- [createCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createCartsStep/index.html.md) +- [addShippingMethodToCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/addShippingMethodToCartStep/index.html.md) +- [createLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemAdjustmentsStep/index.html.md) +- [createPaymentCollectionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createPaymentCollectionsStep/index.html.md) +- [createLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createLineItemsStep/index.html.md) +- [createShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/createShippingMethodAdjustmentsStep/index.html.md) +- [findOrCreateCustomerStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOrCreateCustomerStep/index.html.md) +- [findOneOrAnyRegionStep](https://docs.medusajs.com/references/medusa-workflows/steps/findOneOrAnyRegionStep/index.html.md) +- [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) +- [getPromotionCodesToApply](https://docs.medusajs.com/references/medusa-workflows/steps/getPromotionCodesToApply/index.html.md) +- [getActionsToComputeFromPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getActionsToComputeFromPromotionsStep/index.html.md) +- [getVariantPriceSetsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantPriceSetsStep/index.html.md) +- [prepareAdjustmentsFromPromotionActionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/prepareAdjustmentsFromPromotionActionsStep/index.html.md) +- [getVariantsStep](https://docs.medusajs.com/references/medusa-workflows/steps/getVariantsStep/index.html.md) +- [removeLineItemAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeLineItemAdjustmentsStep/index.html.md) +- [removeShippingMethodAdjustmentsStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodAdjustmentsStep/index.html.md) +- [removeShippingMethodFromCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/removeShippingMethodFromCartStep/index.html.md) +- [retrieveCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/retrieveCartStep/index.html.md) +- [reserveInventoryStep](https://docs.medusajs.com/references/medusa-workflows/steps/reserveInventoryStep/index.html.md) +- [setTaxLinesForItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/setTaxLinesForItemsStep/index.html.md) +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md) +- [updateCartPromotionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartPromotionsStep/index.html.md) +- [updateLineItemsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateLineItemsStep/index.html.md) +- [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) +- [validateCartStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartStep/index.html.md) +- [validateCartShippingOptionsStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateCartShippingOptionsStep/index.html.md) +- [validateLineItemPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateLineItemPricesStep/index.html.md) +- [validateVariantPricesStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateVariantPricesStep/index.html.md) +- [validateShippingStep](https://docs.medusajs.com/references/medusa-workflows/steps/validateShippingStep/index.html.md) # Events Reference @@ -35899,6 +35920,67 @@ npx medusa --help *** +# 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| + + +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + +# 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\`| + + # build Command - Medusa CLI Reference Create a standalone build of the Medusa application. @@ -35961,67 +36043,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| - - -# new Command - Medusa CLI Reference - -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. - -```bash -medusa new [ []] -``` - -## Arguments - -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| - -## Options - -|Option|Description| -|---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| - - # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -36203,6 +36224,22 @@ 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. @@ -36239,22 +36276,6 @@ 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. @@ -36278,84 +36299,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\`| - - # 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). @@ -36372,6 +36315,113 @@ npx medusa exec [file] [args...] |\`args\`|A list of arguments to pass to the function. These arguments are passed in the |No| +# new Command - Medusa CLI Reference + +Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. + +```bash +medusa new [ []] +``` + +## Arguments + +|Argument|Description|Required|Default| +|---|---|---|---|---|---|---| +|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| +|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| + +## Options + +|Option|Description| +|---|---|---| +|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| +|\`--skip-db\`|Skip database creation.| +|\`--skip-env\`|Skip populating | +|\`--db-user \\`|The database user to use for database setup.| +|\`--db-database \\`|The name of the database used for database setup.| +|\`--db-pass \\`|The database password to use for database setup.| +|\`--db-port \\`|The database port to use for database setup.| +|\`--db-host \\`|The database host to use for database setup.| + + +# plugin Commands - Medusa CLI Reference + +Commands starting with `plugin:` perform actions related to [plugin](https://docs.medusajs.com/docs/learn/fundamentals/plugins/index.html.md) development. + +These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). + +## plugin:publish + +Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. + +```bash +npx medusa plugin:publish +``` + +*** + +## plugin:add + +Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. + +```bash +npx medusa plugin:add [names...] +``` + +### Arguments + +|Argument|Description|Required| +|---|---|---|---|---| +|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| + +*** + +## plugin:develop + +Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. + +```bash +npx medusa plugin:develop +``` + +*** + +## plugin:db:generate + +Generate migrations for all modules in a plugin. + +```bash +npx medusa plugin:db:generate +``` + +*** + +## plugin:build + +Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. + +```bash +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.| + + # db Commands - Medusa CLI Reference Commands starting with `db:` perform actions on the database. @@ -36492,111 +36542,66 @@ npx medusa db:sync-links |\`--execute-all\`|Skip prompts when syncing links and execute all (including unsafe) actions.|No|Prompts are shown for unsafe actions, by default.| -# new Command - Medusa CLI Reference +# build Command - Medusa CLI Reference -Create a new Medusa application. Unlike the `create-medusa-app` CLI tool, this command provides more flexibility for experienced Medusa developers in creating and configuring their project. +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 -medusa new [ []] +npx medusa build ``` -## Arguments - -|Argument|Description|Required|Default| -|---|---|---|---|---|---|---| -|\`dir\_name\`|The name of the directory to create the Medusa application in.|Yes|-| -|\`starter\_url\`|The URL of the starter repository to create the project from.|No|\`https://github.com/medusajs/medusa-starter-default\`| +Refer to [this section](#run-built-medusa-application) for next steps. ## Options |Option|Description| |---|---|---| -|\`-y\`|Skip all prompts, such as databaes prompts. A database might not be created if default PostgreSQL credentials don't work.| -|\`--skip-db\`|Skip database creation.| -|\`--skip-env\`|Skip populating | -|\`--db-user \\`|The database user to use for database setup.| -|\`--db-database \\`|The name of the database used for database setup.| -|\`--db-pass \\`|The database password to use for database setup.| -|\`--db-port \\`|The database port to use for database setup.| -|\`--db-host \\`|The database host to use for database setup.| +|\`--admin-only\`|Whether to only build the admin to host it separately. If this option is not passed, the admin is built to the | +*** -# start Command - Medusa CLI Reference +## Run Built Medusa Application -Start the Medusa application in production. +After running the `build` command, use the following step to run the built Medusa application: -```bash -npx medusa start +- Change to the `.medusa/server` directory and install the dependencies: + +```bash npm2yarn +cd .medusa/server && npm install ``` -## Options +- 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. -|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.| +```bash npm2yarn +cp .env .medusa/server/.env.production +``` - -# 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. - -These commands are available starting from [Medusa v2.3.0](https://github.com/medusajs/medusa/releases/tag/v2.3.0). - -## plugin:publish - -Publish a plugin into the local packages registry. The command uses [Yalc](https://github.com/wclr/yalc) under the hood to publish the plugin to a local package registry. You can then install the plugin in a local Medusa project using the [plugin:add](#pluginadd) command. +- In the system environment variables, set `NODE_ENV` to `production`: ```bash -npx medusa plugin:publish +NODE_ENV=production +``` + +- Use the `start` command to run the application: + +```bash npm2yarn +cd .medusa/server && npm run start ``` *** -## plugin:add +## Build Medusa Admin -Install the specified plugins from the local package registry into a local Medusa application. Plugins can be added to the local package registry using the [plugin:publish](#pluginpublish) command. +By default, the Medusa Admin is built to the `.medusa/server/public/admin` directory. -```bash -npx medusa plugin:add [names...] -``` - -### Arguments - -|Argument|Description|Required| -|---|---|---|---|---| -|\`names\`|The names of one or more plugins to install from the local package registry. A plugin's name is as specified in its |Yes| - -*** - -## plugin:develop - -Start a development server for a plugin. The command will watch for changes in the plugin's source code and automatically re-publish the changes into the local package registry. - -```bash -npx medusa plugin:develop -``` - -*** - -## plugin:db:generate - -Generate migrations for all modules in a plugin. - -```bash -npx medusa plugin:db:generate -``` - -*** - -## plugin:build - -Build a plugin before publishing it to NPM. The command will compile an output in the `.medusa/server` directory. - -```bash -npx medusa plugin:build -``` +If you want a separate build to host the admin standalone, such as on Vercel, pass the `--admin-only` option as explained in the [Options](#options) section. This outputs the admin to the `.medusa/admin` directory instead. # telemetry Command - Medusa CLI Reference @@ -36615,6 +36620,22 @@ npx medusa telemetry |\`--disable\`|Disable telemetry.| +# 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\`| + + # user Command - Medusa CLI Reference Create a new admin user. @@ -45787,6 +45808,1141 @@ For a quick access to code snippets of the different concepts you learned about, Deployment guides are a collection of guides that help you deploy your Medusa server and admin to different platforms. Learn more in the [Deployment Overview](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/deployment/index.html.md) documentation. +# Send Abandoned Cart Notifications in Medusa + +In this tutorial, you will learn how to send notifications to customers who have abandoned their carts. + +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. These features include cart-management capabilities. + +Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications. + +In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers. + +## Summary + +By following this tutorial, you will: + +- Install and set up Medusa. +- Create the logic to send an email to customers who have abandoned their carts. +- Run the above logic once a day. +- Add a route to the storefront to recover the cart. + +![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg) + +[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial. + +*** + +## 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 will 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 SendGrid + +### Prerequisites + +- [SendGrid account](https://sendgrid.com) +- [Verified Sender Identity](https://mc.sendgrid.com/senders) +- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) + +Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice. + +To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails. + +Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). + +To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "@medusajs/medusa/notification-sendgrid", + id: "sendgrid", + options: { + channels: ["email"], + api_key: process.env.SENDGRID_API_KEY, + from: process.env.SENDGRID_FROM, + }, + }, + ], + }, + }, + ], +}) +``` + +In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options: + +- `channels`: The channels that the provider supports. In this case, it is only email. +- `api_key`: Your SendGrid API key. +- `from`: The email address that the emails will be sent from. + +Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project: + +```plain +SENDGRID_API_KEY=your-sendgrid-api-key +SENDGRID_FROM=test@gmail.com +``` + +You can now use SendGrid to send emails in Medusa. + +*** + +## Step 3: Send Abandoned Cart Notification Flow + +You will now implement the sending logic for the abandoned cart notifications. + +To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 is 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 a scheduled job. + +In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day. + +The workflow will receive the list of abandoned carts as an input. The workflow has the following steps: + +- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications. +- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date. + +Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one. + +### sendAbandonedNotificationsStep + +The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input. + +To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content: + +```ts title="src/workflows/steps/send-abandoned-notifications.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { Modules } from "@medusajs/framework/utils" +import { CartDTO, CustomerDTO } from "@medusajs/framework/types" + +type SendAbandonedNotificationsStepInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] +} + +export const sendAbandonedNotificationsStep = createStep( + "send-abandoned-notifications", + async (input: SendAbandonedNotificationsStepInput, { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + const notificationData = input.carts.map((cart) => ({ + to: cart.email!, + channel: "email", + template: process.env.ABANDONED_CART_TEMPLATE_ID || "", + data: { + customer: { + first_name: cart.customer?.first_name || cart.shipping_address?.first_name, + last_name: cart.customer?.last_name || cart.shipping_address?.last_name, + }, + cart_id: cart.id, + items: cart.items?.map((item) => ({ + product_title: item.title, + quantity: item.quantity, + unit_price: item.unit_price, + thumbnail: item.thumbnail, + })), + }, + })) + + const notifications = await notificationModuleService.createNotifications( + notificationData + ) + + return new StepResponse({ + notifications, + }) + } +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `create-review`. +2. An async function that receives two parameters: + - The step's input, which is in this case an object with the review's properties. + - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. + +In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method. + +Notice that each notification is an object with the following properties: + +- `to`: The email address of the customer. +- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel. +- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it. +- `data`: The data to pass to the template to render the email's dynamic content. + +Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object. + +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications. + +### Create Workflow + +You can now create the workflow that uses the step you just created to send the abandoned cart notifications. + +Create the file `src/workflows/send-abandoned-carts.ts` with the following content: + +```ts title="src/workflows/send-abandoned-carts.ts" +import { + createWorkflow, + WorkflowResponse, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + sendAbandonedNotificationsStep, +} from "./steps/send-abandoned-notifications" +import { updateCartsStep } from "@medusajs/medusa/core-flows" +import { CartDTO } from "@medusajs/framework/types" +import { CustomerDTO } from "@medusajs/framework/types" + +export type SendAbandonedCartsWorkflowInput = { + carts: (CartDTO & { + customer: CustomerDTO + })[] +} + +export const sendAbandonedCartsWorkflow = createWorkflow( + "send-abandoned-carts", + function (input: SendAbandonedCartsWorkflowInput) { + sendAbandonedNotificationsStep(input) + + const updateCartsData = transform( + input, + (data) => { + return data.carts.map((cart) => ({ + id: cart.id, + metadata: { + ...cart.metadata, + abandoned_notification: new Date().toISOString(), + }, + })) + } + ) + + const updatedCarts = updateCartsStep(updateCartsData) + + return new WorkflowResponse(updatedCarts) + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts. + +In the workflow's constructor function, you: + +- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers. +- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date. + +Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +Your workflow is now ready for use. You will learn how to execute it in the next section. + +### Setup Email Template + +Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step. + +To create an email template in SendGrid: + +- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard. +- Click on the "Create Dynamic Template" button. + +![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png) + +- In the side window that opens, enter a name for the template, then click on the Create button. +- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it. + +![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png) + +In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template. + +You can also use the following template as an example: + +```html title="Abandoned Cart Email Template" + + + + + + Complete Your Purchase + + + +
+
Hi {{customer.first_name}}, your cart is waiting! 🛍️
+

You left some great items in your cart. Complete your purchase before they're gone!

+ + {{#each items}} +
+ {{product_title}} +
+ {{product_title}} +

{{subtitle}}

+

Quantity: {{quantity}}

+

Price: $ {{unit_price}}

+
+
+ {{/each}} + + Return to Cart & Checkout + +
+ + +``` + +This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout. + +You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart. + +Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project: + +```plain +ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id +``` + +*** + +## Step 4: Schedule Cart Abandonment Notifications + +The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers. + +To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. + +You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +import { MedusaContainer } from "@medusajs/framework/types" +import { + sendAbandonedCartsWorkflow, + SendAbandonedCartsWorkflowInput, +} from "../workflows/send-abandoned-carts" + +export default async function abandonedCartJob( + container: MedusaContainer +) { + const logger = container.resolve("logger") + const query = container.resolve("query") + + const oneDayAgo = new Date() + oneDayAgo.setDate(oneDayAgo.getDate() - 1) + const limit = 100 + const offset = 0 + const totalCount = 0 + const abandonedCartsCount = 0 + + do { + // TODO retrieve paginated abandoned carts + } while (offset < totalCount) + + logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`) +} + +export const config = { + name: "abandoned-cart-notification", + schedule: "0 0 * * *", // Run at midnight every day +} +``` + +In a scheduled job's file, you must export: + +1. An asynchronous function that holds 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. +2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs. + +In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. + +You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts. + +Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +const { + data: abandonedCarts, + metadata, +} = await query.graph({ + entity: "cart", + fields: [ + "id", + "email", + "items.*", + "metadata", + "customer.*", + ], + filters: { + updated_at: { + $lt: oneDayAgo, + }, + // @ts-ignore + email: { + $ne: null, + }, + // @ts-ignore + completed_at: null, + }, + pagination: { + skip: offset, + take: limit, + }, +}) + +totalCount = metadata?.count ?? 0 +const cartsWithItems = abandonedCarts.filter((cart) => + cart.items?.length > 0 && !cart.metadata?.abandoned_notification +) + +try { + await sendAbandonedCartsWorkflow(container).run({ + input: { + carts: cartsWithItems, + } as unknown as SendAbandonedCartsWorkflowInput, + }) + abandonedCartsCount += cartsWithItems.length + +} catch (error) { + logger.error( + `Failed to send abandoned cart notification: ${error.message}` + ) +} + +offset += limit +``` + +In the do-while loop, you use Query to retrieve carts matching the following criteria: + +- The cart was last updated more than a day ago. +- The cart has an email address. +- The cart has not been completed. + +You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification. + +Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts. + +### Test it Out + +To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing +``` + +And to change the job's schedule in `config` to run every minute: + +```ts title="src/jobs/send-abandoned-cart-notification.ts" +export const config = { + // ... + schedule: "* * * * *", // Run every minute for testing +} +``` + +Finally, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command: + +```bash npm2yarn +npm run dev +``` + +Open the storefront at `localhost:8000`. You can either: + +- Create an account and add items to the cart, then leave the cart for a minute. +- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute. + +Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal: + +```bash +info: Sent 1 abandoned cart notifications +``` + +Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule. + +*** + +## Step 5: Recover Cart in Storefront + +In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page. + +To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content: + +```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue" +import { NextRequest } from "next/server" +import { retrieveCart } from "../../../../../../lib/data/cart" +import { setCartId } from "../../../../../../lib/data/cookies" +import { notFound, redirect } from "next/navigation" +type Params = Promise<{ + id: string +}> + +export async function GET(req: NextRequest, { params }: { params: Params }) { + const { id } = await params + const cart = await retrieveCart(id) + + if (!cart) { + return notFound() + } + + setCartId(id) + + const countryCode = cart.shipping_address?.country_code || + cart.region?.countries?.[0]?.iso_2 + + redirect( + `/${countryCode ? `${countryCode}/` : ""}cart` + ) +} +``` + +You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you: + +- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response. +- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront. +- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region. + +### Test it Out + +To test it out, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +And in the Next.js Starter Storefront's directory, start the storefront: + +```bash npm2yarn +npm run dev +``` + +Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart. + +![Cart page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1742459552/Medusa%20Resources/Screenshot_2025-03-20_at_10.32.17_AM_frmbup.png) + +*** + +## Next Steps + +You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as: + +- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md). +- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md). +- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md). + +If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have 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 Quick Re-Order Functionality in Medusa + +In this tutorial, you'll learn how to implement a re-order functionality in Medusa. + +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. The features include order-management features. + +The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +## Summary + +By following this tutorial, you'll learn how to: + +- Install and set up Medusa. +- Define the logic to re-order an order. +- Customize the Next.js Starter Storefront to add a re-order button. + +![Diagram showcasing the re-order logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1746451309/Medusa%20Resources/reorder-summary_wnbbws.jpg) + +- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman. + +*** + +## 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: Implement Re-Order Workflow + +To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. + +By using workflows, you can track their 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. + +In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route. + +Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more. + +The workflow will have the following steps: + +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. +- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order. +- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart. +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. + +This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps. + +### a. Create the Workflow + +To create the workflow, create the file `src/workflows/reorder.ts` with the following content: + +```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1} +import { + createWorkflow, + transform, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { + addShippingMethodToCartWorkflow, + createCartWorkflow, + useQueryGraphStep, +} from "@medusajs/medusa/core-flows" + +type ReorderWorkflowInput = { + order_id: string +} + +export const reorderWorkflow = createWorkflow( + "reorder", + ({ order_id }: ReorderWorkflowInput) => { + // @ts-ignore + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "*", + "items.*", + "shipping_address.*", + "billing_address.*", + "region.*", + "sales_channel.*", + "shipping_methods.*", + "customer.*", + ], + filters: { + id: order_id, + }, + }) + + // TODO create a cart with the order's items + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order to re-order. + +In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it. + +### b. Create a Cart + +Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data. + +Replace the `TODO` in the workflow with the following: + +```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2} +const createInput = transform({ + orders, +}, (data) => { + return { + region_id: data.orders[0].region_id!, + sales_channel_id: data.orders[0].sales_channel_id!, + customer_id: data.orders[0].customer_id!, + email: data.orders[0].email!, + billing_address: { + first_name: data.orders[0].billing_address?.first_name!, + last_name: data.orders[0].billing_address?.last_name!, + address_1: data.orders[0].billing_address?.address_1!, + city: data.orders[0].billing_address?.city!, + country_code: data.orders[0].billing_address?.country_code!, + province: data.orders[0].billing_address?.province!, + postal_code: data.orders[0].billing_address?.postal_code!, + phone: data.orders[0].billing_address?.phone!, + }, + shipping_address: { + first_name: data.orders[0].shipping_address?.first_name!, + last_name: data.orders[0].shipping_address?.last_name!, + address_1: data.orders[0].shipping_address?.address_1!, + city: data.orders[0].shipping_address?.city!, + country_code: data.orders[0].shipping_address?.country_code!, + province: data.orders[0].shipping_address?.province!, + postal_code: data.orders[0].shipping_address?.postal_code!, + phone: data.orders[0].shipping_address?.phone!, + }, + items: data.orders[0].items?.map((item) => ({ + variant_id: item?.variant_id!, + quantity: item?.quantity!, + unit_price: item?.unit_price!, + })), + } +}) + +const { id: cart_id } = createCartWorkflow.runAsStep({ + input: createInput, +}) + +// TODO add the shipping method to the cart +``` + +Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data. + +Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). + +`transform` accepts the following parameters: + +1. The data to use in the transformation function. +2. A transformation function that accepts the data from the first parameter and returns the transformed data. + +In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more. + +Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md). + +After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID. + +### c. Add Shipping Methods + +Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again. + +You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart. + +Replace the `TODO` in the workflow with the following: + +```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3} +const addShippingMethodToCartInput = transform({ + cart_id, + orders, +}, (data) => { + return { + cart_id: data.cart_id, + options: data.orders[0].shipping_methods?.map((method) => ({ + id: method?.shipping_option_id!, + data: method?.data!, + })) ?? [], + } +}) + +addShippingMethodToCartWorkflow.runAsStep({ + input: addShippingMethodToCartInput, +}) + +// TODO retrieve and return the cart's details +``` + +Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart. + +Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart. + +### d. Retrieve and Return the Cart's Details + +Finally, you need to retrieve the cart's details and return them as the workflow's output. + +Replace the `TODO` in the workflow with the following: + +```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4} +// @ts-ignore +const { data: carts } = useQueryGraphStep({ + entity: "cart", + fields: [ + "*", + "items.*", + "shipping_methods.*", + "shipping_address.*", + "billing_address.*", + "region.*", + "sales_channel.*", + "promotions.*", + "currency_code", + "subtotal", + "item_total", + "total", + "item_subtotal", + "shipping_subtotal", + "customer.*", + "payment_collection.*", + + ], + filters: { + id: cart_id, + }, +}).config({ name: "retrieve-cart" }) + +return new WorkflowResponse(carts[0]) +``` + +You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method. + +Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`. + +The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case. + +In the next step, you'll create an API route that exposes the re-order functionality. + +*** + +## Step 3: Create Re-Order API Route + +Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this 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, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step. + +Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more. + +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`. + +So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content: + +```ts title="src/api/store/customers/me/orders/[id]/route.ts" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { reorderWorkflow } from "../../../../../../workflows/reorder" + +export async function POST( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) { + const { id } = req.params + + const { result } = await reorderWorkflow(req.scope).run({ + input: { + order_id: id, + }, + }) + + return res.json({ + cart: result, + }) +} +``` + +Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`. + +API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). + +The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as path parameters or authenticated customer details. +2. A response object to manipulate and send the response. + +In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you: + +- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property. + - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations. +- Call the `run` method, passing it an object with the workflow's input. + +You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response. + +You'll test out this API route after you customize the Next.js Starter Storefront. + +*** + +## Step 4: Customize the Next.js Starter Storefront + +In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). + +The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience. + +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-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory: + +```bash +cd ../medusa-reorder-storefront # change based on your project name +``` + +To add the re-order button, you will: + +- Add a server function that re-orders an order using the API route from the previous step. +- Add a button to the order details page that calls the server function. + +### a. Add the Server Function + +You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file. + +First, add the following import statement to the top of the file: + +```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue" +import { setCartId } from "./cookies" +``` + +Then, add the function at the end of the file: + +```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue" +export const reorder = async (id: string) => { + const headers = await getAuthHeaders() + + const { cart } = await sdk.client.fetch( + `/store/customers/me/orders/${id}`, + { + method: "POST", + headers, + } + ) + + await setCartId(cart.id) + + return cart +} +``` + +You add a function that accepts the order ID as a parameter. + +The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step. + +The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it. + +Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront. + +Finally, you return the cart's details. + +### b. Add the Re-Order Button Component + +Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page. + +To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content: + +```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights} +import { Button, toast } from "@medusajs/ui" +import { reorder } from "../../../../lib/data/orders" +import { useState } from "react" +import { useRouter } from "next/navigation" + +type ReorderActionProps = { + orderId: string +} + +export default function ReorderAction({ orderId }: ReorderActionProps) { + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + + const handleReorder = async () => { + setIsLoading(true) + try { + const cart = await reorder(orderId) + + setIsLoading(false) + toast.success("Prepared cart to reorder. Proceeding to checkout...") + router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`) + } catch (error) { + setIsLoading(false) + toast.error(`Error reordering: ${error}`) + } + } + + return ( + + ) +} +``` + +You create a `ReorderAction` component that accepts the order ID as a prop. + +In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order. + +If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message. + +### c. Show Re-Order Button on Order Details Page + +Finally, you'll show the `ReorderAction` component on the order details page. + +In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file: + +```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue" +import ReorderAction from "../components/reorder-action" +``` + +Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it: + +```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +The re-order button will now be shown on the order details page. + +### Test it Out + +You'll now test out the re-order functionality. + +First, to start the Medusa application, run the following command in the Medusa application's directory: + +```bash npm2yarn badgeLabel="Medusa application" badgeColor="green" +npm run dev +``` + +Then, in the Next.js Starter Storefront directory, run the following command to start the storefront: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +The storefront will be running at `http://localhost:8000`. Open it in your browser. + +To test out the re-order functionality: + +- Create an account in the storefront. +- Add a product to the cart and complete the checkout process to place an order. +- Go to Account -> Orders, and click on the "See details" button. + +![Orders page on customer account](https://res.cloudinary.com/dza7lstvk/image/upload/v1746449666/Medusa%20Resources/Screenshot_2025-05-05_at_3.52.46_PM_ae4e78.png) + +On the order's details page, you'll find a "Reorder" button. + +![Reorder button on order details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450255/Medusa%20Resources/Screenshot_2025-05-05_at_4.03.51_PM_xaslqo.png) + +When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase. + +![Checkout page showing the payment step](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450342/Medusa%20Resources/Screenshot_2025-05-05_at_4.05.29_PM_vqpdqo.png) + +*** + +## Next Steps + +You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case. + +For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders. + +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. @@ -49448,659 +50604,33 @@ 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). -# Send Abandoned Cart Notifications in Medusa +# Implement Phone Authentication and Integrate Twilio SMS -In this tutorial, you will learn how to send notifications to customers who have abandoned their carts. +In this tutorial, you will learn how to implement phone number authentication in your Medusa application. -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. These features include cart-management capabilities. +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. These features include authentication with custom providers and for custom user or actor types. -Medusa's [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows you to send notifications to users or customers, such as password reset emails, order confirmation SMS, or other types of notifications. +In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication. -In this tutorial, you will use the Notification Module to send an email to customers who have abandoned their carts. The email will contain a link to recover the customer's cart, encouraging them to complete their purchase. You will use SendGrid to send the emails, but you can also use other email providers. +Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs. ## Summary -By following this tutorial, you will: +By following this tutorial, you will learn how to: - Install and set up Medusa. -- Create the logic to send an email to customers who have abandoned their carts. -- Run the above logic once a day. -- Add a route to the storefront to recover the cart. +- Implement a custom phone authentication provider. +- Integrate Twilio to send OTPs by SMS. +- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers. -![Diagram illustrating the flow of the abandoned-cart functionalities](https://res.cloudinary.com/dza7lstvk/image/upload/v1742460588/Medusa%20Resources/abandoned-cart-summary_fcf2tn.jpg) +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. -[View on Github](https://github.com/medusajs/examples/tree/main/abandoned-cart): Find the full code for this tutorial. +![Diagram showcasing the phone authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747744941/Medusa%20Resources/phone-auth-overview_bt5yrp.jpg) -*** +While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types. -## 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 will 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 SendGrid - -### Prerequisites - -- [SendGrid account](https://sendgrid.com) -- [Verified Sender Identity](https://mc.sendgrid.com/senders) -- [SendGrid API Key](https://app.sendgrid.com/settings/api_keys) - -Medusa's Notification Module provides the general functionality to send notifications, but the sending logic is implemented in a module provider. This allows you to integrate the email provider of your choice. - -To send the cart-abandonment emails, you will use SendGrid. Medusa provides a [SendGrid Notification Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/sendgrid/index.html.md) that you can use to send emails. - -Alternatively, you can use [other Notification Module Providers](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification#what-is-a-notification-module-provider/index.html.md) or [create a custom provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md). - -To set up SendGrid, add the SendGrid Notification Module Provider to `medusa-config.ts`: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "@medusajs/medusa/notification", - options: { - providers: [ - { - resolve: "@medusajs/medusa/notification-sendgrid", - id: "sendgrid", - options: { - channels: ["email"], - api_key: process.env.SENDGRID_API_KEY, - from: process.env.SENDGRID_FROM, - }, - }, - ], - }, - }, - ], -}) -``` - -In the `modules` configuration, you pass the Notification Provider and add SendGrid as a provider. You also pass to the SendGrid Module Provider the following options: - -- `channels`: The channels that the provider supports. In this case, it is only email. -- `api_key`: Your SendGrid API key. -- `from`: The email address that the emails will be sent from. - -Then, set the SendGrid API key and "from" email as environment variables, such as in the `.env` file at the root of your project: - -```plain -SENDGRID_API_KEY=your-sendgrid-api-key -SENDGRID_FROM=test@gmail.com -``` - -You can now use SendGrid to send emails in Medusa. - -*** - -## Step 3: Send Abandoned Cart Notification Flow - -You will now implement the sending logic for the abandoned cart notifications. - -To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). 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 is 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 a scheduled job. - -In this step, you will create the workflow that sends the abandoned cart notifications. Later, you will learn how to execute it once a day. - -The workflow will receive the list of abandoned carts as an input. The workflow has the following steps: - -- [sendAbandonedNotificationsStep](#sendAbandonedNotificationsStep): Send the abandoned cart notifications. -- [updateCartsStep](https://docs.medusajs.com/references/medusa-workflows/steps/updateCartsStep/index.html.md): Update the cart to store the last notification date. - -Medusa provides the second step in its `@medusajs/medusa/core-flows` package. So, you only need to implement the first one. - -### sendAbandonedNotificationsStep - -The first step of the workflow sends a notification to the owners of the abandoned carts that are passed as an input. - -To implement the step, create the file `src/workflows/steps/send-abandoned-notifications.ts` with the following content: - -```ts title="src/workflows/steps/send-abandoned-notifications.ts" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { Modules } from "@medusajs/framework/utils" -import { CartDTO, CustomerDTO } from "@medusajs/framework/types" - -type SendAbandonedNotificationsStepInput = { - carts: (CartDTO & { - customer: CustomerDTO - })[] -} - -export const sendAbandonedNotificationsStep = createStep( - "send-abandoned-notifications", - async (input: SendAbandonedNotificationsStepInput, { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION - ) - - const notificationData = input.carts.map((cart) => ({ - to: cart.email!, - channel: "email", - template: process.env.ABANDONED_CART_TEMPLATE_ID || "", - data: { - customer: { - first_name: cart.customer?.first_name || cart.shipping_address?.first_name, - last_name: cart.customer?.last_name || cart.shipping_address?.last_name, - }, - cart_id: cart.id, - items: cart.items?.map((item) => ({ - product_title: item.title, - quantity: item.quantity, - unit_price: item.unit_price, - thumbnail: item.thumbnail, - })), - }, - })) - - const notifications = await notificationModuleService.createNotifications( - notificationData - ) - - return new StepResponse({ - notifications, - }) - } -) -``` - -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's unique name, which is `create-review`. -2. An async function that receives two parameters: - - The step's input, which is in this case an object with the review's properties. - - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. - -In the step function, you first resolve the Notification Module's service, which has methods to manage notifications. Then, you prepare the data of each notification, and create the notifications with the `createNotifications` method. - -Notice that each notification is an object with the following properties: - -- `to`: The email address of the customer. -- `channel`: The channel that the notification will be sent through. The Notification Module uses the provider registered for the channel. -- `template`: The ID or name of the email template in the third-party provider. Make sure to set it as an environment variable once you have it. -- `data`: The data to pass to the template to render the email's dynamic content. - -Based on the dynamic template you create in SendGrid or another provider, you can pass different data in the `data` object. - -A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts the step's output as a parameter, which is the created notifications. - -### Create Workflow - -You can now create the workflow that uses the step you just created to send the abandoned cart notifications. - -Create the file `src/workflows/send-abandoned-carts.ts` with the following content: - -```ts title="src/workflows/send-abandoned-carts.ts" -import { - createWorkflow, - WorkflowResponse, - transform, -} from "@medusajs/framework/workflows-sdk" -import { - sendAbandonedNotificationsStep, -} from "./steps/send-abandoned-notifications" -import { updateCartsStep } from "@medusajs/medusa/core-flows" -import { CartDTO } from "@medusajs/framework/types" -import { CustomerDTO } from "@medusajs/framework/types" - -export type SendAbandonedCartsWorkflowInput = { - carts: (CartDTO & { - customer: CustomerDTO - })[] -} - -export const sendAbandonedCartsWorkflow = createWorkflow( - "send-abandoned-carts", - function (input: SendAbandonedCartsWorkflowInput) { - sendAbandonedNotificationsStep(input) - - const updateCartsData = transform( - input, - (data) => { - return data.carts.map((cart) => ({ - id: cart.id, - metadata: { - ...cart.metadata, - abandoned_notification: new Date().toISOString(), - }, - })) - } - ) - - const updatedCarts = updateCartsStep(updateCartsData) - - return new WorkflowResponse(updatedCarts) - } -) -``` - -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. - -It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an arra of carts. - -In the workflow's constructor function, you: - -- Use the `sendAbandonedNotificationsStep` to send the notifications to the carts' customers. -- Use the `updateCartsStep` from Medusa's core flows to update the carts' metadata with the last notification date. - -Notice that you use the `transform` function to prepare the `updateCartsStep`'s input. Medusa does not support direct data manipulation in a workflow's constructor function. You can learn more about it in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -Your workflow is now ready for use. You will learn how to execute it in the next section. - -### Setup Email Template - -Before you can test the workflow, you need to set up an email template in SendGrid. The template should contain the dynamic content that you pass in the workflow's step. - -To create an email template in SendGrid: - -- Go to [Dynamic Templates](https://mc.sendgrid.com/dynamic-templates) in the SendGrid dashboard. -- Click on the "Create Dynamic Template" button. - -![Button is at the top right of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742457153/Medusa%20Resources/Screenshot_2025-03-20_at_9.51.38_AM_g5nk80.png) - -- In the side window that opens, enter a name for the template, then click on the Create button. -- The template will be added to the middle of the page. When you click on it, a new section will show with an "Add Version" button. Click on it. - -![The template is a collapsible in the middle of the page,with the "Add Version" button shown in the middle](https://res.cloudinary.com/dza7lstvk/image/upload/v1742458096/Medusa%20Resources/Screenshot_2025-03-20_at_10.07.54_AM_y2dys7.png) - -In the form that opens, you can either choose to start with a blank template or from an existing design. You can then use the drag-and-drop or code editor to design the email template. - -You can also use the following template as an example: - -```html title="Abandoned Cart Email Template" - - - - - - Complete Your Purchase - - - -
-
Hi {{customer.first_name}}, your cart is waiting! 🛍️
-

You left some great items in your cart. Complete your purchase before they're gone!

- - {{#each items}} -
- {{product_title}} -
- {{product_title}} -

{{subtitle}}

-

Quantity: {{quantity}}

-

Price: $ {{unit_price}}

-
-
- {{/each}} - - Return to Cart & Checkout - -
- - -``` - -This template will show each item's image, title, quantity, and price in the cart. It will also show a button to return to the cart and checkout. - -You can replace `https://yourstore.com` with your storefront's URL. You'll later implement the `/cart/recover/:cart_id` route in the storefront to recover the cart. - -Once you are done, copy the template ID from SendGrid and set it as an environment variable in your Medusa project: - -```plain -ABANDONED_CART_TEMPLATE_ID=your-sendgrid-template-id -``` - -*** - -## Step 4: Schedule Cart Abandonment Notifications - -The next step is to automate sending the abandoned cart notifications. You need a task that runs once a day to find the carts that have been abandoned for a certain period and send the notifications to the customers. - -To run a task at a scheduled interval, you can use a [scheduled job](https://docs.medusajs.com/docs/learn/fundamentals/scheduled-jobs/index.html.md). A scheduled job is an asynchronous function that the Medusa application runs at the interval you specify during the Medusa application's runtime. - -You can create a scheduled job in a TypeScript or JavaScript file under the `src/jobs` directory. So, to create the scheduled job that sends the abandoned cart notifications, create the file `src/jobs/send-abandoned-cart-notification.ts` with the following content: - -```ts title="src/jobs/send-abandoned-cart-notification.ts" -import { MedusaContainer } from "@medusajs/framework/types" -import { - sendAbandonedCartsWorkflow, - SendAbandonedCartsWorkflowInput, -} from "../workflows/send-abandoned-carts" - -export default async function abandonedCartJob( - container: MedusaContainer -) { - const logger = container.resolve("logger") - const query = container.resolve("query") - - const oneDayAgo = new Date() - oneDayAgo.setDate(oneDayAgo.getDate() - 1) - const limit = 100 - const offset = 0 - const totalCount = 0 - const abandonedCartsCount = 0 - - do { - // TODO retrieve paginated abandoned carts - } while (offset < totalCount) - - logger.info(`Sent ${abandonedCartsCount} abandoned cart notifications`) -} - -export const config = { - name: "abandoned-cart-notification", - schedule: "0 0 * * *", // Run at midnight every day -} -``` - -In a scheduled job's file, you must export: - -1. An asynchronous function that holds 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. -2. A `config` object that specifies the job's name and schedule. The schedule is a [cron expression](https://crontab.guru/) that defines the interval at which the job runs. - -In the scheduled job function, so far you resolve the [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) to log messages, and [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to retrieve data across modules. - -You also define a `oneDayAgo` date, which is the date that you will use as the condition of an abandoned cart. In addition, you define variables to paginate the carts. - -Next, you will retrieve the abandoned carts using Query. Replace the `TODO` with the following: - -```ts title="src/jobs/send-abandoned-cart-notification.ts" -const { - data: abandonedCarts, - metadata, -} = await query.graph({ - entity: "cart", - fields: [ - "id", - "email", - "items.*", - "metadata", - "customer.*", - ], - filters: { - updated_at: { - $lt: oneDayAgo, - }, - // @ts-ignore - email: { - $ne: null, - }, - // @ts-ignore - completed_at: null, - }, - pagination: { - skip: offset, - take: limit, - }, -}) - -totalCount = metadata?.count ?? 0 -const cartsWithItems = abandonedCarts.filter((cart) => - cart.items?.length > 0 && !cart.metadata?.abandoned_notification -) - -try { - await sendAbandonedCartsWorkflow(container).run({ - input: { - carts: cartsWithItems, - } as unknown as SendAbandonedCartsWorkflowInput, - }) - abandonedCartsCount += cartsWithItems.length - -} catch (error) { - logger.error( - `Failed to send abandoned cart notification: ${error.message}` - ) -} - -offset += limit -``` - -In the do-while loop, you use Query to retrieve carts matching the following criteria: - -- The cart was last updated more than a day ago. -- The cart has an email address. -- The cart has not been completed. - -You also filter the retrieved carts to only include carts with items and customers that have not received an abandoned cart notification. - -Finally, you execute the `sendAbandonedCartsWorkflow` passing it the abandoned carts as an input. You will execute the workflow for each paginated batch of carts. - -### Test it Out - -To test out the scheduled job and workflow, it is recommended to change the `oneDayAgo` date to a minute before now for easy testing: - -```ts title="src/jobs/send-abandoned-cart-notification.ts" -oneDayAgo.setMinutes(oneDayAgo.getMinutes() - 1) // For testing -``` - -And to change the job's schedule in `config` to run every minute: - -```ts title="src/jobs/send-abandoned-cart-notification.ts" -export const config = { - // ... - schedule: "* * * * *", // Run every minute for testing -} -``` - -Finally, start the Medusa application with the following command: - -```bash npm2yarn -npm run dev -``` - -And in the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md)'s directory (that you installed in the first step), start the storefront with the following command: - -```bash npm2yarn -npm run dev -``` - -Open the storefront at `localhost:8000`. You can either: - -- Create an account and add items to the cart, then leave the cart for a minute. -- Add an item to the cart as a guest. Then, start the checkout process, but only enter the shipping and email addresses, and leave the cart for a minute. - -Afterwards, wait for the job to execute. Once it is executed, you will see the following message in the terminal: - -```bash -info: Sent 1 abandoned cart notifications -``` - -Once you're done testing, make sure to revert the changes to the `oneDayAgo` date and the job's schedule. - -*** - -## Step 5: Recover Cart in Storefront - -In the storefront, you need to add a route that recovers the cart when the customer clicks on the link in the email. The route should receive the cart ID, set the cart ID in the cookie, and redirect the customer to the cart page. - -To implement the route, in the Next.js Starter Storefront create the file `src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx` with the following content: - -```tsx title="src/app/[countryCode]/(main)/cart/recover/[id]/route.tsx" badgeLabel="Storefront" badgeColor="blue" -import { NextRequest } from "next/server" -import { retrieveCart } from "../../../../../../lib/data/cart" -import { setCartId } from "../../../../../../lib/data/cookies" -import { notFound, redirect } from "next/navigation" -type Params = Promise<{ - id: string -}> - -export async function GET(req: NextRequest, { params }: { params: Params }) { - const { id } = await params - const cart = await retrieveCart(id) - - if (!cart) { - return notFound() - } - - setCartId(id) - - const countryCode = cart.shipping_address?.country_code || - cart.region?.countries?.[0]?.iso_2 - - redirect( - `/${countryCode ? `${countryCode}/` : ""}cart` - ) -} -``` - -You add a `GET` route handler that receives the cart ID as a path parameter. In the route handler, you: - -- Try to retrieve the cart from the Medusa application. The `retrieveCart` function is already available in the Next.js Starter Storefront. If the cart is not found, you return a 404 response. -- Set the cart ID in a cookie using the `setCartId` function. This is also a function that is already available in the storefront. -- Redirect the customer to the cart page. You set the country code in the URL based on the cart's shipping address or region. - -### Test it Out - -To test it out, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -And in the Next.js Starter Storefront's directory, start the storefront: - -```bash npm2yarn -npm run dev -``` - -Then, either open the link in an abandoned cart email or navigate to `localhost:8000/cart/recover/:cart_id` in your browser. You will be redirected to the cart page with the recovered cart. - -![Cart page in the storefront](https://res.cloudinary.com/dza7lstvk/image/upload/v1742459552/Medusa%20Resources/Screenshot_2025-03-20_at_10.32.17_AM_frmbup.png) - -*** - -## Next Steps - -You have now implemented the logic to send abandoned cart notifications in Medusa. You can implement other customizations with Medusa, such as: - -- [Implement Product Reviews](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/index.html.md). -- [Implement Wishlist](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/plugins/guides/wishlist/index.html.md). -- [Allow Custom-Item Pricing](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/examples/guides/custom-item-price/index.html.md). - -If you are new to Medusa, check out the [main documentation](https://docs.medusajs.com/docs/learn/index.html.md), where you will get a more in-depth learning of all the concepts you have 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 Quick Re-Order Functionality in Medusa - -In this tutorial, you'll learn how to implement a re-order functionality in Medusa. - -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. The features include order-management features. - -The Medusa Framework facilitates building custom features that are necessary for your business use case. In this tutorial, you'll learn how to implement a re-order functionality in Medusa. This feature is useful for businesses whose customers are likely to repeat their orders, such as B2B or food delivery businesses. - -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. - -## Summary - -By following this tutorial, you'll learn how to: - -- Install and set up Medusa. -- Define the logic to re-order an order. -- Customize the Next.js Starter Storefront to add a re-order button. - -![Diagram showcasing the re-order logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1746451309/Medusa%20Resources/reorder-summary_wnbbws.jpg) - -- [Re-Order Repository](https://github.com/medusajs/examples/tree/main/re-order): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1741941475/OpenApi/product-reviews_jh8ohj.yaml): Import this OpenApi Specs file into tools like Postman. +- [Phone Authentication Repository](https://github.com/medusajs/examples/tree/main/phone-auth): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1747745832/OpenApi/Phone_Auth_g4xsqv.yaml): Import this OpenApi Specs file into tools like Postman. *** @@ -50120,468 +50650,2008 @@ 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. +Afterward, 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. +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: Implement Re-Order Workflow +## Step 2: Implement Phone Authentication Module Provider -To build custom commerce features in Medusa, you create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of queries and actions, called steps, that complete a task. +In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-providers/index.html.md). Then, you can use that provider to authenticate users using custom logic. -By using workflows, you can track their 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. +In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers. -In this section, you'll implement the re-order functionality in a workflow. Later, you'll execute the workflow in a custom API route. +An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) documentation. -Refer to the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) to learn more. +### Prerequisite: Install jsonwebtoken -The workflow will have the following steps: +In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the order's details. -- [createCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md): Create a cart for the re-order. -- [addShippingMethodToCartWorkflow](https://docs.medusajs.com/references/medusa-workflows/addShippingMethodToCartWorkflow/index.html.md): Add the order's shipping method(s) to the cart. -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve the cart's details. +To install the package, run the following command in the Medusa application directory: -This workflow uses steps from Medusa's `@medusajs/medusa/core-flows` package. So, you can implement the workflow without implementing custom steps. +```bash npm2yarn +npm install jsonwebtoken +npm install @types/jsonwebtoken --save-dev +``` -### a. Create the Workflow +### a. Create Module Directory -To create the workflow, create the file `src/workflows/reorder.ts` with the following content: +Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`. -```ts title="src/workflows/reorder.ts" highlights={workflowHighlights1} +### b. Create Auth Module Provider Service + +A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users. + +To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content: + +```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights} import { - createWorkflow, - transform, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" + AbstractAuthModuleProvider, + AbstractEventBusModuleService, +} from "@medusajs/framework/utils" import { - addShippingMethodToCartWorkflow, - createCartWorkflow, - useQueryGraphStep, -} from "@medusajs/medusa/core-flows" + Logger, +} from "@medusajs/types" -type ReorderWorkflowInput = { - order_id: string +type InjectedDependencies = { + logger: Logger + event_bus: AbstractEventBusModuleService } -export const reorderWorkflow = createWorkflow( - "reorder", - ({ order_id }: ReorderWorkflowInput) => { +type Options = { + jwtSecret: string +} + +class PhoneAuthService extends AbstractAuthModuleProvider { + static DISPLAY_NAME = "Phone Auth" + static identifier = "phone-auth" + private options: Options + private logger: Logger + private event_bus: AbstractEventBusModuleService + + constructor(container: InjectedDependencies, options: Options) { // @ts-ignore - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "*", - "items.*", - "shipping_address.*", - "billing_address.*", - "region.*", - "sales_channel.*", - "shipping_methods.*", - "customer.*", - ], - filters: { - id: order_id, + super(...arguments) + + this.options = options + this.logger = container.logger + this.event_bus = container.event_bus + } +} + +export default PhoneAuthService +``` + +An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps. + +An Authentication Module Provider must also have the following static properties: + +- `identifier`: A unique identifier for the provider. +- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes. + +A module provider's constructor receives two parameters: + +- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You access the following resources: + - `logger`: A [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) class to log debug messages. + - `event_bus`: The [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md)'s service to emit events. +- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option: + - `jwtSecret`: A secret used to sign and verify the OTPs. + +You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations). + +In the constructor, you set the class's properties to the injected dependencies and options. + +In the next sections, you'll implement the methods of the `AbstractAuthModuleProvider` class. + +Refer to the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider/index.html.md) guide for detailed information about the methods. + +### c. Implement validateOptions Method + +The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start. + +So, add the `validateOptions` method to the `PhoneAuthService` class: + +```ts title="src/modules/phone-auth/service.ts" +// other imports... +import { + MedusaError, +} from "@medusajs/framework/utils" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + static validateOptions(options: Record): void | never { + if (!options.jwtSecret) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "JWT secret is required" + ) + } + } +} +``` + +The `validateOptions` method receives the options passed to the module provider as a parameter. + +In the method, you throw an error if the `jwtSecret` option is not set. + +### d. Implement register Method + +When a customer (or another actor type) registers in your application, they must also have an [auth identity](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-identity-and-actor-types/index.html.md) that allows them to login. + +The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity. + +Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/auth-flows/index.html.md) documentation. + +![Diagram showcasing the relation between a customer and auth identity](https://res.cloudinary.com/dza7lstvk/image/upload/v1747747179/Medusa%20Resources/customer-auth-identity_je1bvh.jpg) + +So, add the `register` method to the `PhoneAuthService` class: + +```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights} +// other imports... +import { + AuthenticationInput, + AuthIdentityProviderService, + AuthenticationResponse, +} from "@medusajs/types" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async register( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone } = data.body || {} + + if (!phone) { + return { + success: false, + error: "Phone number is required", + } + } + + try { + await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + + return { + success: false, + error: "User with phone number already exists", + } + } catch (error) { + const user = await authIdentityProviderService.create({ + entity_id: phone, + }) + + return { + success: true, + authIdentity: user, + } + } + } +} +``` + +#### Parameters + +The `register` method receives an object parameter with the following properties: + +- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body. +- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities. + +The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#register/index.html.md) guide. + +#### Method Logic + +In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number. + +Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier. + +#### Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the registration was successful. +- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful. +- `error`: An error message if the registration failed. + +### e. Implement authenticate Method + +When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user. + +Authentication providers may implement one of the following flows: + +- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password. +- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information. + +For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity. + +![Diagram showcasing authentication with callback verification](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749683/Medusa%20Resources/authentication-flow_ey59hw.jpg) + +So, add the `authenticate` method to the `PhoneAuthService` class: + +```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights} +// other imports... +import { + AuthIdentityDTO, +} from "@medusajs/types" +import jwt from "jsonwebtoken" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async authenticate( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone } = data.body || {} + + if (!phone) { + return { + success: false, + error: "Phone number is required", + } + } + + try { + await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + } catch (error) { + return { + success: false, + error: "User with phone number does not exist", + } + } + + const { hashedOTP, otp } = await this.generateOTP() + + await authIdentityProviderService.update(phone, { + provider_metadata: { + otp: hashedOTP, }, }) - // TODO create a cart with the order's items + await this.event_bus.emit({ + name: "phone-auth.otp.generated", + data: { + otp, + phone, + }, + }, {}) + + return { + success: true, + location: "otp", + } } -) -``` -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + async generateOTP(): Promise<{ hashedOTP: string, otp: string }> { + // Generate a 6-digit OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString() -It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is an object holding the ID of the order to re-order. - -In the workflow's constructor function, so far you use the `useQueryGraphStep` step to retrieve the order's details. This step uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) under the hood, which allows you to query data across [modules](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - -Refer to the [Query documentation](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) to learn more about how to use it. - -### b. Create a Cart - -Next, you need to create a cart using the old order's details. You can use the `createCartWorkflow` step to create a cart, but you first need to prepare its input data. - -Replace the `TODO` in the workflow with the following: - -```ts title="src/workflows/reorder.ts" highlights={workflowHighlights2} -const createInput = transform({ - orders, -}, (data) => { - return { - region_id: data.orders[0].region_id!, - sales_channel_id: data.orders[0].sales_channel_id!, - customer_id: data.orders[0].customer_id!, - email: data.orders[0].email!, - billing_address: { - first_name: data.orders[0].billing_address?.first_name!, - last_name: data.orders[0].billing_address?.last_name!, - address_1: data.orders[0].billing_address?.address_1!, - city: data.orders[0].billing_address?.city!, - country_code: data.orders[0].billing_address?.country_code!, - province: data.orders[0].billing_address?.province!, - postal_code: data.orders[0].billing_address?.postal_code!, - phone: data.orders[0].billing_address?.phone!, - }, - shipping_address: { - first_name: data.orders[0].shipping_address?.first_name!, - last_name: data.orders[0].shipping_address?.last_name!, - address_1: data.orders[0].shipping_address?.address_1!, - city: data.orders[0].shipping_address?.city!, - country_code: data.orders[0].shipping_address?.country_code!, - province: data.orders[0].shipping_address?.province!, - postal_code: data.orders[0].shipping_address?.postal_code!, - phone: data.orders[0].shipping_address?.phone!, - }, - items: data.orders[0].items?.map((item) => ({ - variant_id: item?.variant_id!, - quantity: item?.quantity!, - unit_price: item?.unit_price!, - })), - } -}) - -const { id: cart_id } = createCartWorkflow.runAsStep({ - input: createInput, -}) - -// TODO add the shipping method to the cart -``` - -Data manipulation is not allowed in a workflow, as Medusa stores its definition before executing it. Instead, you can use `transform` from the Workflows SDK to manipulate the data. - -Learn more about why you can't manipulate data in a workflow and the `transform` function in the [Data Manipulation in Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/variable-manipulation/index.html.md). - -`transform` accepts the following parameters: - -1. The data to use in the transformation function. -2. A transformation function that accepts the data from the first parameter and returns the transformed data. - -In the above code snippet, you use `transform` to create the input for the `createCartWorkflow` step. The input is an object that holds the cart's details, including its items, shipping and billing addresses, and more. - -Learn about other input parameters you can pass in the [createCartWorkflow reference](https://docs.medusajs.com/references/medusa-workflows/createCartWorkflow/index.html.md). - -After that, you execute the `createCartWorkflow` passing it the transformed input. The workflow returns the cart's details, including its ID. - -### c. Add Shipping Methods - -Next, you need to add the order's shipping method(s) to the cart. This saves the customer from having to select a shipping method again. - -You can use the `addShippingMethodToCartWorkflow` step to add the shipping method(s) to the cart. - -Replace the `TODO` in the workflow with the following: - -```ts title="src/workflows/reorder.ts" highlights={workflowHighlights3} -const addShippingMethodToCartInput = transform({ - cart_id, - orders, -}, (data) => { - return { - cart_id: data.cart_id, - options: data.orders[0].shipping_methods?.map((method) => ({ - id: method?.shipping_option_id!, - data: method?.data!, - })) ?? [], - } -}) - -addShippingMethodToCartWorkflow.runAsStep({ - input: addShippingMethodToCartInput, -}) - -// TODO retrieve and return the cart's details -``` - -Again, you use `transform` to prepare the input for the `addShippingMethodToCartWorkflow`. The input includes the cart's ID and the shipping method(s) to add to the cart. - -Then, you execute the `addShippingMethodToCartWorkflow` to add the shipping method(s) to the cart. - -### d. Retrieve and Return the Cart's Details - -Finally, you need to retrieve the cart's details and return them as the workflow's output. - -Replace the `TODO` in the workflow with the following: - -```ts title="src/workflows/reorder.ts" highlights={workflowHighlights4} -// @ts-ignore -const { data: carts } = useQueryGraphStep({ - entity: "cart", - fields: [ - "*", - "items.*", - "shipping_methods.*", - "shipping_address.*", - "billing_address.*", - "region.*", - "sales_channel.*", - "promotions.*", - "currency_code", - "subtotal", - "item_total", - "total", - "item_subtotal", - "shipping_subtotal", - "customer.*", - "payment_collection.*", + // for debug + this.logger.info(`Generated OTP: ${otp}`) - ], - filters: { - id: cart_id, - }, -}).config({ name: "retrieve-cart" }) - -return new WorkflowResponse(carts[0]) -``` - -You execute the `useQueryGraphStep` again to retrieve the cart's details. Since you're re-using a step, you have to rename it using the `config` method. - -Finally, you return the cart's details. A workflow must return an instance of `WorkflowResponse`. - -The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is the cart's details in this case. - -In the next step, you'll create an API route that exposes the re-order functionality. - -*** - -## Step 3: Create Re-Order API Route - -Now that you have the logic to re-order, you need to expose it so that frontend clients, such as a storefront, can use it. You do this 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, such as storefronts. You'll create an API route at the path `/store/customers/me/orders/:id` that executes the workflow from the previous step. - -Refer to the [API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md) to learn more. - -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`. - -So, create the file `src/api/store/customers/me/orders/[id]/route.ts` with the following content: - -```ts title="src/api/store/customers/me/orders/[id]/route.ts" -import { - AuthenticatedMedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" -import { reorderWorkflow } from "../../../../../../workflows/reorder" - -export async function POST( - req: AuthenticatedMedusaRequest, - res: MedusaResponse -) { - const { id } = req.params - - const { result } = await reorderWorkflow(req.scope).run({ - input: { - order_id: id, - }, - }) - - return res.json({ - cart: result, - }) + const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, { + expiresIn: "60s", + }) + + return { hashedOTP, otp } + } } ``` -Since you export a `POST` route handler function, you expose a `POST` API route at `/store/customers/me/orders/:id`. +You add two methods: the `authenticate` method, and a helper `generateOTP` method. -API routes that start with `/store/customers/me` are protected by default, meaning that only authenticated customers can access them. Learn more in the [Protected API Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/protected-routes/index.html.md). +#### authenticate Parameters -The route handler function accepts two parameters: +The `authenticate` method receives an object parameter with the following properties: -1. A request object with details and context on the request, such as path parameters or authenticated customer details. -2. A response object to manipulate and send the response. +- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body. +- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities. -In the route handler function, you execute the `reorderWorkflow`. To execute a workflow, you: +The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#authenticate/index.html.md) guide. -- Invoke it, passing it the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md) available in the `req.scope` property. - - The Medusa container is a registry of Framework and commerce resources that you can resolve and use in your customizations. -- Call the `run` method, passing it an object with the workflow's input. +#### authenticate Logic -You pass the order ID from the request's path parameters as the workflow's input. Finally, you return the created cart's details in the response. +In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist. -You'll test out this API route after you customize the Next.js Starter Storefront. +Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio. + +The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity. + +Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio. + +#### authenticate Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the authentication was successful. +- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP. +- `error`: An error message if the authentication failed. + +### f. Implement validateCallback Method + +When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method. + +You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application. + +![Diagram showcasing how callback verification fits in the authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749734/Medusa%20Resources/callback-flow_yney5a.jpg) + +So, add the `validateCallback` method to the `PhoneAuthService` class: + +```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights} +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async validateCallback( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone, otp } = data.query || {} + + if (!phone || !otp) { + return { + success: false, + error: "Phone number and OTP are required", + } + } + + const user = await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + + if (!user) { + return { + success: false, + error: "User with phone number does not exist", + } + } + + // verify that OTP is correct + const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier) + if (!userProvider || !userProvider.provider_metadata?.otp) { + return { + success: false, + error: "User with phone number does not have a phone auth provider", + } + } + + try { + const decodedOTP = jwt.verify( + userProvider.provider_metadata.otp as string, + this.options.jwtSecret + ) as { otp: string } + + if (decodedOTP.otp !== otp) { + throw new Error("Invalid OTP") + } + } catch (error) { + return { + success: false, + error: error.message || "Invalid OTP", + } + } + + const updatedUser = await authIdentityProviderService.update(phone, { + provider_metadata: { + otp: null, + }, + }) + + return { + success: true, + authIdentity: updatedUser, + } + } +} +``` + +#### Parameters + +The `validateCallback` method receives an object parameter with the following properties: + +- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query. +- `authIdentityProviderService`: A service injected by the [Auth Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/auth/index.html.md) that allows you to manage auth identities. + +The method receives other parameters, which you can find in the [Create Auth Module Provider](https://docs.medusajs.com/references/auth/provider#validatecallback/index.html.md) guide. + +#### Method Logic + +In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist. + +Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error. + +Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP. + +After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property. + +#### Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the authentication was successful. +- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful. +- `error`: An error message if the authentication failed. + +### g. Export Module Definition + +You've now finished implementing the necessary methods for the Phone Authentication Module Provider. + +The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders. + +To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content: + +```ts title="src/modules/phone-auth/index.ts" +import PhoneAuthService from "./service" +import { + ModuleProvider, + Modules, +} from "@medusajs/framework/utils" + +export default ModuleProvider(Modules.AUTH, { + services: [PhoneAuthService], +}) +``` + +You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters: + +1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case. +2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa. + +### h. Add Module Provider to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property: + +```ts title="medusa-config.ts" +// other imports... +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [ + Modules.CACHE, + ContainerRegistrationKeys.LOGGER, + Modules.EVENT_BUS, + ], + options: { + providers: [ + // default provider + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + }, + { + resolve: "./src/modules/phone-auth", + id: "phone-auth", + options: { + jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret", + }, + }, + ], + }, + }, + ], +}) +``` + +To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value. + +The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/cache/index.html.md) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider. + +The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers. + +To register the Phone Authentication Module Provider, you add an object to the `providers` array with the following properties: + +- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/phone-auth` directory. +- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`. +- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service. + +### i. Enable Phone Authentication for Customers + +By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it. + +In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property: + +```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights} +module.exports = defineConfig({ + projectConfig: { + // ... + http: { + // ... + authMethodsPerActor: { + user: ["emailpass"], + customer: ["emailpass", "phone-auth"], + }, + }, + }, + // ... +}) +``` + +The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type. + +In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors. + +### Test Out Phone Authentication + +In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront). + +First, start the Medusa application with the following command: + +```bash npm2yarn +npm run start +``` + +#### Prerequisite: Retrieve Publishable API Key + +Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`. + +To retrieve the publishable API key: + +1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in. +2. Go to Settings -> Publishable API Keys. +3. Click on the API key in the table. +4. In its details page, click on the API key to copy it. + +![Publishable API Key page with the API key clicked](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/User%20Guide/Screenshot_2025-02-25_at_6.14.15_PM_muwq9e.png) + +#### a. Retrieve Registration Token + +The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application. + +To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \ +--header 'Content-Type: application/json' \ +--data '{ + "phone": "+19077890116" +}' +``` + +Make sure to replace the phone number with the one you want to use. + +This will return a `token` in the response: + +```json title="Example Response" +{ + "token": "123..." +} +``` + +#### b. Register Customer + +Next, you'll register the customer using the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request. + +So, send a `POST` request to `/store/customers`: + +```bash +curl -X POST 'http://localhost:9000/store/customers' \ +--header 'x-publishable-api-key: {publishable_api_key}' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer {reg_token}' \ +--data-raw '{ + "email": "+19077890116@gmail.com", + "phone": "19077890116", + "first_name": "John", + "last_name": "Smith" +}' +``` + +Make sure to replace: + +- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard. +- `{reg_token}` with the registration token you received in the previous step. +- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step. + - You pass the email because it's required by the [Register Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain. + +The request will return the created customer's details: + +```json title="Example Response" +{ + "customer": { + "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC", + "email": "+19077890116@gmail.com", + "company_name": null, + "first_name": "John", + "last_name": "Smith", + "phone": "19077890116", + "metadata": null, + "has_account": true, + "deleted_at": null, + "created_at": "2025-05-20T09:01:13.273Z", + "updated_at": "2025-05-20T09:01:13.273Z", + "addresses": [] + } +} +``` + +The customer can now authenticate using the phone number and OTP. + +#### c. Authenticate Customer + +Next, you'll authenticate the customer using the [Authenticate Customer](https://docs.medusajs.com/api/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step). + +So, send a `POST` request to `/auth/customer/phone-auth`: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \ +--header 'Content-Type: application/json' \ +--data '{ + "phone": "+19077890116" +}' +``` + +Make sure to replace the phone number with the one you used to register the customer. + +This will return a `location` in the response: + +```json title="Example Response" +{ + "location": "otp" +} +``` + +Indicating that the user should verify their OTP. + +You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent. + +#### d. Verify OTP + +If you check the logs of the Medusa application, you'll see that the OTP was generated and logged: + +```bash +info: Generated OTP: 576794 +``` + +As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio. + +So, to verify the OTP, you'll send a request to the [Verify Callback API route](https://docs.medusajs.com/api/store#auth_postactor_typeauth_providercallback): + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588' +``` + +You pass the following query parameters: + +- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`. +- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs. + +If the OTP is valid, you'll receive a JWT token in the response: + +```json title="Example Response" +{ + "token": "123..." +} +``` + +You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](https://docs.medusajs.com/api/store#customers_getcustomersme). + +If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP *** -## Step 4: Customize the Next.js Starter Storefront +## Step 3: Integrate Twilio SMS -In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to add a re-order button. You installed the Next.js Starter Storefront in the first step with the Medusa application, but you can also install it separately as explained in the [Next.js Starter Storefront documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md). +Similar to the Auth Module, the [Notification Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/notification/index.html.md) allows registering custom providers to send notifications, such as SMS or email. -The Next.js Starter Storefront provides rich commerce features and a sleek design. You can use it as-is or build on top of it to tailor it for your business's unique use case, design, and customer experience. +In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer. + +### Prerequisites + +- [Twilio Account](https://console.twilio.com/) +- [Twilio From Phone Number](https://www.twilio.com/docs/phone-numbers) +- [Twilio Account SID, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page) +- [Twilio Auth Token, which you can retrieve from the Twilio Console homepage.](https://www.twilio.com/docs/usage/tutorials/how-to-use-your-free-trial-account-namer#console-dashboard-home-page) + +### a. Install Twilio SDK + +Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API. + +Run the following command in the Medusa application directory: + +```bash npm2yarn +npm install twilio +``` + +You'll use the Twilio SDK in the Notification Module Provider's service. + +### b. Create Module Directory + +Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider. + +### c. Create Notification Module Provider Service + +A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class. + +So, create the file `src/modules/twilio-sms/service.ts` with the following content: + +```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights} +import { + AbstractNotificationProviderService, +} from "@medusajs/framework/utils" +import { Twilio } from "twilio" + +type InjectedDependencies = {} + +type TwilioSmsServiceOptions = { + accountSid: string + authToken: string + from: string +} + +class TwilioSmsService extends AbstractNotificationProviderService { + static readonly identifier = "twilio-sms" + private readonly client: Twilio + private readonly from: string + + constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) { + super() + + this.client = new Twilio(options.accountSid, options.authToken) + this.from = options.from + } +} +``` + +You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps. + +A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application. + +A module provider's constructor receives two parameters: + +- `container`: The [module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md) that contains Framework resources available to the module. You don't need to access any resources for this provider. +- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option: + - `accountSid`: The Twilio account SID. + - `authToken`: The Twilio auth token. + - `from`: The Twilio phone number to send the SMS from. + +You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations). + +In the constructor, you set the class's properties to the injected dependencies and options. + +In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class. + +Refer to the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) guide for detailed information about the methods. + +### d. Implement validateOptions Method + +The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start. + +So, add the `validateOptions` method to the `TwilioSmsService` class: + +```ts title="src/modules/twilio-sms/service.ts" +class TwilioSmsService extends AbstractNotificationProviderService { + // ... + static validateOptions(options: Record): void | never { + if (!options.accountSid) { + throw new Error("Account SID is required") + } + if (!options.authToken) { + throw new Error("Auth token is required") + } + if (!options.from) { + throw new Error("From is required") + } + } +} +``` + +The `validateOptions` method receives the options passed to the module provider as a parameter. + +In the method, you throw an error if any of the options are not set. + +### e. Implement send Method + +The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider. + +So, add the `send` method to the `TwilioSmsService` class: + +```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights} +// other imports... +import { + ProviderSendNotificationDTO, + ProviderSendNotificationResultsDTO, +} from "@medusajs/types" + +class TwilioSmsService extends AbstractNotificationProviderService { + // ... + async send( + notification: ProviderSendNotificationDTO + ): Promise { + const { to, content, template, data } = notification + const contentText = content?.text || await this.getTemplateContent( + template, data + ) + + const message = await this.client.messages.create({ + body: contentText, + from: this.from, + to, + }) + + return { + id: message.sid, + } + } + + async getTemplateContent( + template: string, + data?: Record | null + ): Promise { + switch (template) { + case "otp-template": + if (!data?.otp) { + throw new Error("OTP is required for OTP template") + } + + return `Your OTP is ${data.otp}` + default: + throw new Error(`Template ${template} not found`) + } + } +} +``` + +You implement the `send` method and a helper `getTemplateContent` method. + +#### send Parameters + +The `send` method receives an object parameter with the following properties: + +- `to`: The phone number to send the SMS to. +- `content`: An object containing the content of the SMS. The `text` property is the text to send. +- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided. +- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values. + +The method receives other parameters, which you can find in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module#send/index.html.md) guide. + +#### send Method Logic + +In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template. + +Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters: + +- `body`: The content of the SMS. +- `from`: The Twilio phone number to send the SMS from. +- `to`: The phone number to send the SMS to. + +#### send Return Value + +Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database. + +### f. Export Module Definition + +You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition. + +To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content: + +```ts title="src/modules/twilio-sms/index.ts" +import { + ModuleProvider, + Modules, +} from "@medusajs/framework/utils" +import TwilioSMSNotificationService from "./service" + +export default ModuleProvider(Modules.NOTIFICATION, { + services: [TwilioSMSNotificationService], +}) +``` + +You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it two parameters: + +1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case. +2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as notification providers in Medusa. + +### g. Add Module Provider to Medusa's Configurations + +You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it. + +In `medusa-config.ts`, add the following to the `modules` property: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // default provider + { + resolve: "@medusajs/medusa/notification-local", + id: "local", + options: { + name: "Local Notification Provider", + channels: ["feed"], + }, + }, + { + resolve: "./src/modules/twilio-sms", + id: "twilio-sms", + options: { + channels: ["sms"], + accountSid: process.env.TWILIO_ACCOUNT_SID, + authToken: process.env.TWILIO_AUTH_TOKEN, + from: process.env.TWILIO_FROM, + }, + }, + ], + }, + }, + ], +}) +``` + +You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider. + +The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers. + +To register the Twilio Notification Module Provider, you add an object with the following properties: + +- `resolve`: The path to the module provider. +- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`. +- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service. + - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications. + - `accountSid`: The Twilio account SID. + - `authToken`: The Twilio auth token. + - `from`: The Twilio phone number to send the SMS from. + +### h. Add Environment Variables + +To set the value of the Twilio options, add the following environment variables to your `.env` file: + +```shell title=".env" +TWILIO_ACCOUNT_SID=AC... +TWILIO_AUTH_TOKEN=05... +TWILIO_FROM=+1... +``` + +Where: + +- `TWILIO_ACCOUNT_SID`: The Twilio account SID. +- `TWILIO_AUTH_TOKEN`: The Twilio auth token. +- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio. + +You can retrieve these information from the Twilio Console homepage. + +![Twilio console homepage showing the account SID, phone number, and auth token](https://res.cloudinary.com/dza7lstvk/image/upload/v1747736641/Medusa%20Resources/CleanShot_2025-05-20_at_13.22.55_2x_sztmfo.png) + +### i. Handle OTP Generated Event + +Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider. + +You can listen to events in a [subscriber](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted. + +In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP. + +Refer to the [Events and Subscribers](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md) documentation to learn more. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content: + +```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights} +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { Modules } from "@medusajs/framework/utils" + +export default async function sendOtpHandler({ + event: { data: { + phone, + otp, + } }, + container, +}: SubscriberArgs<{ phone: string, otp: string }>) { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + await notificationModuleService.createNotifications({ + to: phone, + channel: "sms", + template: "otp-template", + data: { + otp, + }, + }) +} + +export const config: SubscriberConfig = { + event: "phone-auth.otp.generated", +} +``` + +The subscriber file must export: + +- An asynchronous subscriber function that's executed whenever the associated event is triggered. +- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`. + +The subscriber function accepts an object with the following properties: + +- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data: + - `phone`: The phone number of the user. + - `otp`: The OTP that was generated. +- `container`: The [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which you can use to resolve Framework and commerce resources. + +In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user. + +Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case. + +The `createNotifications` method accepts an object with the following properties: + +- `to`: The phone number to send the notification to. You use the phone number from the event's data payload. +- `channel`: The channel to use to send the notification, which is `sms`. +- `template`: The template to use for the notification content, which is `otp-template`. +- `data`: An object containing the data to use in the template. You pass the OTP to the template. + +### j. Test it Out + +To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section. + +After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token. + +Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step. + +Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio. + +*** + +## Step 4: Use Phone Authentication in the Next.js Starter Storefront + +In this step, you'll customize the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) to allow customers to authenticate using their phone number and OTP. + +By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to. 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-reorder`, you can find the storefront by going back to the parent directory and changing to the `medusa-reorder-storefront` directory: +So, if your Medusa application's directory is `medusa-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory: ```bash -cd ../medusa-reorder-storefront # change based on your project name +cd ../medusa-phone-auth-storefront # change based on your project name ``` -To add the re-order button, you will: +### a. Install Phone Input Package -- Add a server function that re-orders an order using the API route from the previous step. -- Add a button to the order details page that calls the server function. +To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package: -### a. Add the Server Function - -You'll add the server function for the re-order functionality in the `src/lib/data/orders.ts` file. - -First, add the following import statement to the top of the file: - -```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue" -import { setCartId } from "./cookies" +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install react-phone-number-input ``` -Then, add the function at the end of the file: +You'll use it in the login and registration forms to show a phone input. -```ts title="src/lib/data/orders.ts" badgeLabel="Storefront" badgeColor="blue" -export const reorder = async (id: string) => { - const headers = await getAuthHeaders() +### b. Add Authenticate Function - const { cart } = await sdk.client.fetch( - `/store/customers/me/orders/${id}`, - { - method: "POST", - headers, +Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer. + +The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number. + +In `src/lib/data/customer.ts`, add the following function: + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" +export const authenticateWithPhone = async (phone: string) => { + try { + const response = await sdk.auth.login("customer", "phone-auth", { + phone, + }) + + if ( + typeof response === "string" || + !response.location || + response.location !== "otp" + ) { + throw new Error("Failed to login") } - ) - await setCartId(cart.id) - - return cart + return true + } catch (error: any) { + return error.toString() + } } ``` -You add a function that accepts the order ID as a parameter. +The function accepts the phone number as a parameter. -The function uses the `client.fetch` method of the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to send a request to the API route you created in the previous step. +In the function, you use the [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body. -The JS SDK is already configured in the Next.js Starter Storefront. Refer to the [JS SDK documentation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) to learn more about it. +If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful. -Once the request succeeds, you use the `setCartId` function that's defined in the storefront to set the cart ID in a cookie. This ensures the cart is used across the storefront. +### c. add Verify OTP Function -Finally, you return the cart's details. +Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP. -### b. Add the Re-Order Button Component +In `src/lib/data/customer.ts`, add the following function: -Next, you'll add the component that shows the re-order button. You'll later add the component to the order details page. +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights} +export const verifyOtp = async ({ + otp, + phone, +}: { + otp: string + phone: string +}) => { + try { + const token = await sdk.auth.callback("customer", "phone-auth", { + phone, + otp, + }) -To create the component, create the file `src/modules/order/components/reorder-action/index.tsx` with the following content: + await setAuthToken(token) -```tsx title="src/modules/order/components/reorder-action/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={componentHighlights} -import { Button, toast } from "@medusajs/ui" -import { reorder } from "../../../../lib/data/orders" -import { useState } from "react" -import { useRouter } from "next/navigation" + const customerCacheTag = await getCacheTag("customers") + revalidateTag(customerCacheTag) -type ReorderActionProps = { - orderId: string + await transferCart() + + return true + } catch (e: any) { + return e.toString() + } +} +``` + +The function accepts an object with the following properties: + +- `otp`: The OTP to verify. +- `phone`: The phone number of the customer. + +In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body. + +If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page. + +Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer. + +Finally, you return `true` to indicate that the request was successful. + +### d. Add Registration Function + +The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number. + +In `src/lib/data/customer.ts`, add the following function: + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights} +export const registerWithPhone = async ({ + firstName, + lastName, + phone, +}: { + firstName: string + lastName: string + phone: string +}) => { + try { + const { token: regToken } = await sdk.client.fetch< + { token: string } + >(`/auth/customer/phone-auth/register`, { + method: "POST", + body: { + phone, + }, + }) + + await setAuthToken(regToken as string) + const headers = { + ...(await getAuthHeaders()), + } + + const email = `${phone}@gmail.com` + const customerData = { + email, + first_name: firstName, + last_name: lastName, + phone, + } + + await sdk.store.customer.create( + customerData, + {}, + headers + ) + + return await authenticateWithPhone(phone) + } catch (error: any) { + return error.toString() + } +} +``` + +The function accepts an object with the following properties: + +- `firstName`: The first name of the customer. +- `lastName`: The last name of the customer. +- `phone`: The phone number of the customer. + +In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body. + +Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](https://docs.medusajs.com/api/store#customers_postcustomers) API route. You pass the following properties in the request body: + +- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain. +- `first_name`: The first name of the customer. +- `last_name`: The last name of the customer. +- `phone`: The phone number of the customer. + +Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login. + +### e. Add OTP Form + +Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages. + +Create the file `src/modules/account/components/otp/index.tsx` with the following content: + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights} +"use client" + +import { Input } from "@medusajs/ui" +import { useState, useRef, useEffect } from "react" +import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer" +import ErrorMessage from "../../../checkout/components/error-message" + +type Props = { + phone: string } -export default function ReorderAction({ orderId }: ReorderActionProps) { - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() +export const Otp = ({ phone }: Props) => { + const [otp, setOtp] = useState("") + const [error, setError] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [countdown, setCountdown] = useState(60) + const inputRefs = useRef<(HTMLInputElement | null)[]>([]) - const handleReorder = async () => { + const handleSubmit = async () => { setIsLoading(true) - try { - const cart = await reorder(orderId) + const response = await verifyOtp({ + otp, + phone, + }) + setOtp("") + setIsLoading(false) - setIsLoading(false) - toast.success("Prepared cart to reorder. Proceeding to checkout...") - router.push(`/${cart.shipping_address!.country_code}/checkout?step=payment`) - } catch (error) { - setIsLoading(false) - toast.error(`Error reordering: ${error}`) + if (typeof response === "string") { + setError(response) } } - return ( - - ) + const handleResend = async () => { + authenticateWithPhone(phone) + setCountdown(60) + } + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault() + const pastedData = e.clipboardData.getData("text") + const numericValue = pastedData.replace(/\D/g, "").slice(0, 6) + + if (numericValue) { + setOtp(numericValue) + // Focus the next empty input after pasted content + const nextEmptyIndex = Math.min(numericValue.length, 5) + inputRefs.current[nextEmptyIndex]?.focus() + } + } + + // TODO add use effects } ``` -You create a `ReorderAction` component that accepts the order ID as a prop. +You create an `Otp` component that accepts the phone number as a prop. -In the component, you render a button that, when clicked, calls a `handleReorder` function. The function calls the `reorder` function you created in the previous step to re-order the order. +In the component, you define the following state variables: -If the re-order succeeds, you redirect the user to the payment step of the checkout page. If it fails, you show an error message. +- `otp`: The OTP entered by the customer. +- `error`: The error message to show if the OTP verification fails. +- `isLoading`: A boolean indicating whether the OTP verification is in progress. +- `countdown`: The countdown timer for resending the OTP. +- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits. -### c. Show Re-Order Button on Order Details Page +You also define the following functions: -Finally, you'll show the `ReorderAction` component on the order details page. +- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer. +- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number. +- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually. -In `src/modules/order/templates/order-details-template.tsx`, add the following import statement to the top of the file: +#### Handle Variable Changes -```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue" -import ReorderAction from "../components/reorder-action" +Next, you'll add `useEffect` hooks to handle changes in the state variables. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights} +useEffect(() => { + if (inputRefs.current[0]) { + inputRefs.current[0].focus() + } +}, [inputRefs.current]) + +useEffect(() => { + if (otp.length !== 6 || isLoading) { + return + } + + handleSubmit() +}, [otp, isLoading]) + +useEffect(() => { + const timer = setInterval(() => { + setCountdown((prev) => { + return prev > 0 ? prev - 1 : 0 + }) + }, 1000) + + return () => clearInterval(timer) +}, []) + +// TODO render form ``` -Then, in the return statement of the `OrderDetailsTemplate` component, find the `OrderDetails` component and add the `ReorderAction` component below it: +You add three `useEffect` hooks: -```tsx title="src/modules/order/templates/order-details-template.tsx" badgeLabel="Storefront" badgeColor="blue" - +1. The first one focuses the first input element when the component mounts. +2. The second one automatically submits the OTP when the customer enters six digits. +3. The third one adds an interval to update the countdown timer every second. + +### f. Render OTP Form + +Lastly, you'll render the OTP form with the input elements and the resend button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

+ Verify Phone Number +

+

+ Enter the code sent to your phone number to login. +

+
+ {[...Array(6)].map((_, index) => ( + { + inputRefs.current[index] = el + }} + onPaste={handlePaste} + value={otp[index] || ""} + onChange={(e) => { + const elm = e.target + const value = elm.value + setOtp((prev) => { + const newOtp = prev.split("") + newOtp[index] = value + return newOtp.join("") + }) + if (value && /^\d+$/.test(value)) { + // Move focus to next input + const nextInput = elm.parentElement?.nextElementSibling?.querySelector("input") + nextInput?.focus() + } + }} + onKeyDown={(e) => { + if (e.key === "Backspace" && !e.currentTarget.value) { + // Move focus to previous input on backspace + const prevInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input") + prevInput?.focus() + } + }} + /> + ))} +
+
+ +
+ +
+) ``` -The re-order button will now be shown on the order details page. +You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input. + +You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero. + +You now have an OTP form that you can use in both the login and registration pages. + +### g. Add Registration Form + +You'll now add a registration form that allows the customer to register using their phone number. + +First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]} +export enum LOGIN_VIEW { + SIGN_IN = "sign-in", + REGISTER = "register", + REGISTER_PHONE = "register-phone", + SIGN_IN_PHONE = "sign-in-phone", +} +``` + +By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number. + +Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content: + +```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights} +"use client" + +import { useState } from "react" +import Input from "@modules/common/components/input" +import { LOGIN_VIEW } from "@modules/account/templates/login-template" +import ErrorMessage from "@modules/checkout/components/error-message" +import LocalizedClientLink from "@modules/common/components/localized-client-link" +import { registerWithPhone } from "@lib/data/customer" +import "react-phone-number-input/style.css" +import PhoneInput from "react-phone-number-input" +import { Otp } from "../otp" +import { useParams } from "next/navigation" +import { Button } from "@medusajs/ui" + +type Props = { + setCurrentView: (view: LOGIN_VIEW) => void +} + +const RegisterPhone = ({ setCurrentView }: Props) => { + const [firstName, setFirstName] = useState("") + const [lastName, setLastName] = useState("") + const [phone, setPhone] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + const [enterOtp, setEnterOtp] = useState(false) + const { countryCode } = useParams() as { countryCode: string } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + const response = await registerWithPhone({ + firstName, + lastName, + phone, + }) + setLoading(false) + if (typeof response === "string") { + setError(response) + return + } + + setEnterOtp(true) + } + + if (enterOtp) { + return + } + + // TODO render form +} + +export default RegisterPhone +``` + +You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views. + +In the component, you define the following state variables: + +- `firstName`, `lastName`, and `phone` to store the form inputs' values. +- `error`: The error message to show if the registration fails. +- `loading`: A boolean indicating whether the registration is in progress. +- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP. +- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country. + +You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number. + +If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message. + +#### Render Registration Form + +Next, you'll render the registration form with the input fields and the submit button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

+ Become a Medusa Store Member +

+

+ Create your Medusa Store Member profile, and get access to an enhanced + shopping experience. +

+
+
+ setFirstName(e.target.value)} + /> + setLastName(e.target.value)} + /> + setPhone(value as string)} + name="phone" + required + autoComplete="off" + // @ts-ignore + defaultCountry={countryCode.toUpperCase()} + /> +
+ + + By creating an account, you agree to Medusa Store's{" "} + + Privacy Policy + {" "} + and{" "} + + Terms of Use + + . + + + + + Already a member?{" "} + + . + +
+) +``` + +You render the registration form with input fields for the first name, last name, and phone number. + +For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters. + +You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form. + +#### Add to Login Template + +Next, you'll add the `RegisterPhone` component to the login template. + +In `src/modules/account/templates/login-template.tsx`, add the following import: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +import RegisterPhone from "@modules/account/components/register-phone" +``` + +Then, change the `return` statement of the `LoginTemplate` component to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+ {currentView === "sign-in" ? ( + + ) : currentView === "register" ? ( + + ) : currentView === "register-phone" ? ( + + ) : ( + // TODO: Add login phone view + <> + )} +
+) +``` + +You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later. + +### h. Add Login Form + +Next, you'll add a login form that allows the customer to log in using their phone number. + +To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content: + +```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights} +"use client" + +import { authenticateWithPhone } from "@lib/data/customer" +import { LOGIN_VIEW } from "@modules/account/templates/login-template" +import ErrorMessage from "@modules/checkout/components/error-message" +import { useState } from "react" +import "react-phone-number-input/style.css" +import PhoneInput from "react-phone-number-input" +import { Otp } from "../otp" +import { useParams } from "next/navigation" +import { Button } from "@medusajs/ui" + +type Props = { + setCurrentView: (view: LOGIN_VIEW) => void +} + +const LoginPhone = ({ setCurrentView }: Props) => { + const [phone, setPhone] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + const [enterOtp, setEnterOtp] = useState(false) + const { countryCode } = useParams() as { countryCode: string } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + const response = await authenticateWithPhone(phone) + setLoading(false) + if (typeof response === "string") { + setError(response) + return + } + + setEnterOtp(true) + } + + if (enterOtp) { + return + } + + // TODO render form +} + +export default LoginPhone +``` + +You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views. + +In the component, you define the following state variables: + +- `phone`: The phone number entered by the customer. +- `error`: The error message to show if the login fails. +- `loading`: A boolean indicating whether the login is in progress. +- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted. +- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country. + +You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form. + +#### Render Login Form + +Next, you'll render the login form with the input field and the submit button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

Welcome back

+

+ Sign in to access an enhanced shopping experience. +

+
+
+ setPhone(value as string)} + name="phone" + required + // @ts-ignore + defaultCountry={countryCode.toUpperCase()} + /> +
+ {error && } + + + + Not a member?{" "} + + . + +
+) +``` + +You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked. + +### i. Add to Login Template + +Next, you'll add the `LoginPhone` component to the login template. + +In `src/modules/account/templates/login-template.tsx`, add the following import: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +import LoginPhone from "../components/login-phone" +``` + +Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE) +``` + +This ensures the phone login form is shown by default. + +Finally, replace the `return` statement of the `LoginTemplate` component to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+ {currentView === "sign-in" ? ( + + ) : currentView === "register" ? ( + + ) : currentView === "register-phone" ? ( + + ) : ( + + )} +
+) +``` + +You show the login form when the `currentView` is set to `sign-in-phone`. ### Test it Out -You'll now test out the re-order functionality. +You can now test out the phone authentication feature in the Next.js Starter Storefront. -First, to start the Medusa application, run the following command in the Medusa application's directory: +First, start the Medusa application by running the following command in the Medusa project's directory: -```bash npm2yarn badgeLabel="Medusa application" badgeColor="green" +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" npm run dev ``` -Then, in the Next.js Starter Storefront directory, run the following command to start the storefront: +Then, start the Next.js Starter Storefront by running the following command in the storefront project's directory: ```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" npm run dev ``` -The storefront will be running at `http://localhost:8000`. Open it in your browser. +Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input. -To test out the re-order functionality: +![Login form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739945/Medusa%20Resources/CleanShot_2025-05-20_at_14.18.36_2x_ubuika.png) -- Create an account in the storefront. -- Add a product to the cart and complete the checkout process to place an order. -- Go to Account -> Orders, and click on the "See details" button. +You can also switch to the registration form by clicking on the "Join" link below the login form. -![Orders page on customer account](https://res.cloudinary.com/dza7lstvk/image/upload/v1746449666/Medusa%20Resources/Screenshot_2025-05-05_at_3.52.46_PM_ae4e78.png) +![Registration form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739988/Medusa%20Resources/CleanShot_2025-05-20_at_14.19.35_2x_btlc2s.png) -On the order's details page, you'll find a "Reorder" button. +You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS. -![Reorder button on order details page](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450255/Medusa%20Resources/Screenshot_2025-05-05_at_4.03.51_PM_xaslqo.png) +![OTP form](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740102/Medusa%20Resources/CleanShot_2025-05-20_at_14.21.18_2x_yrkuyg.png) -When you click on the button, a new cart will be created with the order's details, and you'll be redirected to the checkout page where you can complete the purchase. +After you enter the six digits, you'll be logged in and you'll see your profile page. -![Checkout page showing the payment step](https://res.cloudinary.com/dza7lstvk/image/upload/v1746450342/Medusa%20Resources/Screenshot_2025-05-05_at_4.05.29_PM_vqpdqo.png) +![Profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740166/Medusa%20Resources/CleanShot_2025-05-20_at_14.22.30_2x_kshb83.png) + +*** + +## Step 5: Disallow Phone Updates + +The phone authentication feature is now complete, but there are two improvements you can make: + +1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set. +2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication. + +### a. Show Phone Number in Profile Page + +To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement: + +```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue" + + {customer?.email} + +``` + +And replace it with the following: + +```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue" + + {customer?.phone} + +``` + +If you check the profile page now, you'll see the phone number instead of the email address at the top right. + +![Phone number showing in profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740378/Medusa%20Resources/CleanShot_2025-05-20_at_14.25.59_2x_x0pxvt.png) + +### b. Remove Email and Phone Fields + +Next, you'll remove the fields to update email and phone number from the profile page. + +In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement: + +```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue" +
+ {/* ... */} + {/* Remove the following */} + + + + + {/* ... */} +
+``` + +If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed. + +![Profile page without email and phone fields](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740515/Medusa%20Resources/CleanShot_2025-05-20_at_14.28.09_2x_ugab7d.png) + +### c. Disable Phone Updates in Medusa + +While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API. + +In this section, you'll add a [middleware](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) to the `/store/customers/me` API route that prevents customers from updating their phone number. + +A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more. + +Learn more in the [Middlewares](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md) documentation. + +To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green" +import { defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: ["POST"], + middlewares: [ + async (req, res, next) => { + const { phone } = req.body as Record + + if (phone) { + return res.status(400).json({ + error: "Phone number is not allowed to be updated", + }) + } + + next() + }, + ], + }, + ], +}) +``` + +You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes. + +Each object in `routes` has the following properties: + +- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`. +- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route. +- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API. + +Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number. *** ## Next Steps -You now have a re-order functionality in your Medusa application and Next.js Starter Storefront. You can expand more on this feature based on your use case. +You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case. -For example, you can add quick orders on the storefront's homepage, allowing customers to quickly re-order their last orders. +### Authenticate Other Actor Types + +This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors. + +To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + http: { + // ... + authMethodsPerActor: { + user: ["emailpass", "phone-auth"], + customer: ["emailpass", "phone-auth"], + vendor: ["emailpass", "phone-auth"], + }, + }, + }, +}) +``` + +Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate: + +- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register` +- `/auth/customer/phone-auth` -> `/auth/user/phone-auth` +- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback` + +Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations. + +The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users. + +### Learn More about Medusa 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). +### Troubleshooting + +If you encounter issues during your development, check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/index.html.md). + +### Getting Help + +If you encounter issues not covered in the troubleshooting guides: + +1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions. +2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members. +3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. + # Use Saved Payment Methods During Checkout @@ -51501,6 +53571,7 @@ A Notification Module Provider sends messages to users and customers in your Med - [SendGrid](https://docs.medusajs.com/infrastructure-modules/notification/sendgrid/index.html.md) - [Resend](https://docs.medusajs.com/integrations/guides/resend/index.html.md) +- [Twilio SMS](https://docs.medusajs.com/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms/index.html.md) Learn how to integrate a third-party notification provider in the [Create Notification Module Provider](https://docs.medusajs.com/references/notification-provider-module/index.html.md) documentation. @@ -51523,1222 +53594,6 @@ Integrate a search engine to index and search products or other types of data in - [Algolia](https://docs.medusajs.com/integrations/guides/algolia/index.html.md) -# Integrate Algolia (Search) with Medusa - -In this tutorial, you'll learn how to integrate Medusa with Algolia. - -When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as a search engine, allowing you to build your unique requirements around core commerce flows. - -[Algolia](https://www.algolia.com/doc/) is a search engine that enables you to build and manage an intuitive search experience for your customers. By integrating Algolia with Medusa, you can index e-commerce data, such as products, and allow clients to search through them. - -You can follow this guide whether you're new to Medusa or an advanced Medusa developer. - -## Summary - -By following this tutorial, you'll learn how to: - -- Install and set up Medusa. -- Integrate Algolia into Medusa. -- Trigger Algolia reindexing when a product is created, updated, deleted, or when the admin manually triggers a reindex. -- Customize the Next.js Starter Storefront to allow searching for products through Algolia. - -![Diagram illustrating the integration of Algolia with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1742889842/Medusa%20Resources/algolia-summary_lhegrr.jpg) - -- [Algolia Integration Repository](https://github.com/medusajs/examples/tree/main/algolia-integration): Find the full code for this guide in this repository. -- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1742829748/OpenApi/Algolia-Search_t1zlkd.yaml): Import this OpenApi Specs file into tools like Postman. - -*** - -## 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 named `{project-name}-storefront`. - -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: Create Algolia Module - -To integrate third-party services into 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 custom module that provides the necessary functionalities to integrate Algolia with Medusa. - -Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. - -Before building the module, you need to install Algolia's JavaScript client. Run the following command in your Medusa application's root directory: - -```bash npm2yarn -npm install algoliasearch -``` - -### Create Module Directory - -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/algolia`. - -### Create Service - -You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. - -In this section, you'll create the Algolia Module's service and the methods necessary to manage indexed products in Algolia and search through them. - -To create the Algolia Module's service, create the file `src/modules/algolia/service.ts` with the following content: - -```ts title="src/modules/algolia/service.ts" -import { algoliasearch, SearchClient } from "algoliasearch" - -type AlgoliaOptions = { - apiKey: string; - appId: string; - productIndexName: string; -} - -export type AlgoliaIndexType = "product" - -export default class AlgoliaModuleService { - private client: SearchClient - private options: AlgoliaOptions - - constructor({}, options: AlgoliaOptions) { - this.client = algoliasearch(options.appId, options.apiKey) - this.options = options - } - - // TODO add methods -} -``` - -You export a class that will be the Algolia Module's main service. In the service, you define two properties: - -- `client`: An instance of the Algolia Search Client, which you'll use to perform actions with Algolia's API. -- `options`: An object of options that the Module receives when it's registered, which you'll learn about later. The options contain: - - `apiKey`: The Algolia API key. - - `appId`: The Algolia App ID. - - `productIndexName`: The name of the index where products are stored. - -If you want to index other types of data, such as product categories, you can add new properties for their index names in the `AlgoliaOptions` type. - -A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Algolia client using the module's options. - -A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the [Module Container documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). - -#### Index Data Method - -The first method you need to add to the servie is a method that receives an array of data to add or update in Algolia's index. - -Add the following methods to the `AlgoliaModuleService` class: - -```ts title="src/modules/algolia/service.ts" -export default class AlgoliaModuleService { - // ... - async getIndexName(type: AlgoliaIndexType) { - switch (type) { - case "product": - return this.options.productIndexName - default: - throw new Error(`Invalid index type: ${type}`) - } - } - - async indexData(data: Record[], type: AlgoliaIndexType = "product") { - const indexName = await this.getIndexName(type) - this.client.saveObjects({ - indexName, - objects: data.map((item) => ({ - ...item, - // set the object ID to allow updating later - objectID: item.id, - })), - }) - } -} -``` - -You define two methods: - -1. `getIndexName`: A method that receives an `AlgoliaIndexType` (defined in the previous snippt) and returns the index name for that type. In this case, you only have one type, `product`, so you return the product index name. - - If you want to index other types of data, you can add more cases to the switch statement. -2. `indexData`: A method that receives an array of data and an `AlgoliaIndexType`. The method indexes the data in the Algolia index for the given type. - - Notice that you set the `objectID` property of each object to the object's `id`. This ensures that you later update the object instead of creating a new one. - -#### Retrieve and Delete Methods - -The next methods you'll add to the service are methods to retrieve and delete data from the Algolia index. You'll see their use later as you keep the Algolia index in sync with Medusa. - -Add the following methods to the `AlgoliaModuleService` class: - -```ts title="src/modules/algolia/service.ts" -export default class AlgoliaModuleService { - // ... - - async retrieveFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") { - const indexName = await this.getIndexName(type) - return await this.client.getObjects>({ - requests: objectIDs.map((objectID) => ({ - indexName, - objectID, - })), - }) - } - - async deleteFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") { - const indexName = await this.getIndexName(type) - await this.client.deleteObjects({ - indexName, - objectIDs, - }) - } -} -``` - -You define two methods: - -1. `retrieveFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method retrieves the objects with the given IDs from the Algolia index. -2. `deleteFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method deletes the objects with the given IDs from the Algolia index. - -#### Search Method - -The last method you'll implement is a method to search through the Algolia index. You'll later use this method to expose the search functionality to clients, such as the Next.js Starter Storefront. - -Add the following method to the `AlgoliaModuleService` class: - -```ts title="src/modules/algolia/service.ts" -export default class AlgoliaModuleService { - // ... - - async search(query: string, type: AlgoliaIndexType = "product") { - const indexName = await this.getIndexName(type) - return await this.client.search({ - requests: [ - { - indexName, - query, - }, - ], - }) - } -} -``` - -The `search` method receives a query string and an `AlgoliaIndexType`. The method searches through the Algolia index for the given type, such as products, and returns the results. - -### 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/algolia/index.ts` with the following content: - -```ts title="src/modules/algolia/index.ts" -import { Module } from "@medusajs/framework/utils" -import AlgoliaModuleService from "./service" - -export const ALGOLIA_MODULE = "algolia" - -export default Module(ALGOLIA_MODULE, { - service: AlgoliaModuleService, -}) -``` - -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 `algolia`. -2. An object with a required property `service` indicating the module's service. - -You also export the module's name as `ALGOLIA_MODULE` so you can reference it later. - -### Add Module to Medusa's Configurations - -Once you finish building the module, add it to Medusa's configurations to start using it. - -In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: - -```ts title="medusa-config.ts" -module.exports = defineConfig({ - // ... - modules: [ - { - resolve: "./src/modules/algolia", - options: { - appId: process.env.ALGOLIA_APP_ID!, - apiKey: process.env.ALGOLIA_API_KEY!, - productIndexName: process.env.ALGOLIA_PRODUCT_INDEX_NAME!, - }, - }, - ], -}) -``` - -Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. - -You also pass an `options` property with the module's options, including the Algolia App ID, API Key, and the product index name. - -### Add Environment Variables - -Before you can start using the Algolia Module, you need to set the environment variables for the Algolia App ID, API Key, and the product index name. - -Add the following environment variables to your `.env` file: - -```env -ALGOLIA_APP_ID=your-algolia-app-id -ALGOLIA_API_KEY=your-algolia-api-key -ALGOLIA_PRODUCT_INDEX_NAME=your-product-index-name -``` - -Where: - -- `your-algolia-app-id` is your Algolia App ID. You can retrieve it from the Algolia dashboard by clicking at the application ID at the top left next to the sidebar. The pop up will show the application ID below the application's name. - -![Find the Algolia App ID by clicking on the application name in the Algolia dashboard, then copying the ID below the name in the pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815360/Medusa%20Resources/Screenshot_2025-03-24_at_1.19.30_PM_kdp3y5.png) - -- `your-algolia-api-key` is your Algolia API Key. To retrieve it from the Algolia dashboard: - 1. Click on Settings in the sidebar. - 2. Choose API Keys under "Team and Access". - -![In the settings page, find the Team and Access section at the right of the page and choose API Keys](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815534/Medusa%20Resources/Screenshot_2025-03-24_at_1.25.09_PM_hwsiba.png) - -3. Copy the Admin API Key. - -- `your-product-index-name` is the name of the index where you'll store products. You can find it by going to Search -> Index, and copying the index name at the top of the page. - -![In the Algolia dashboard, go to Search -> Index and copy the index name at the top of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815790/Medusa%20Resources/Screenshot_2025-03-24_at_1.28.58_PM_yq10sf.png) - -Your module is now ready for use. You'll see how to use it in the next steps. - -*** - -## Step 3: Sync Products to Algolia Workflow - -To keep the Algolia index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow the admin to manually trigger a reindex. - -To implement the indexing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of 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. - -Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). - -In this step, you'll create a workflow that indexes products in Algolia. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when the admin manually triggers a reindex. - -The workflow has the following steps: - -- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products matching specified filters and pagination parameters. -- [syncProductsStep](#syncProductsStep): Index products in Algolia. - -Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step. - -### syncProductsStep - -In the second step of the workflow, you create or update indexes in Algolia for the products retrieved in the first step. - -To create the step, create the file `src/workflows/steps/sync-products.ts` with the following content: - -```ts title="src/workflows/steps/sync-products.ts" -import { ProductDTO } from "@medusajs/framework/types" -import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { ALGOLIA_MODULE } from "../../modules/algolia" -import AlgoliaModuleService from "../../modules/algolia/service" - -export type SyncProductsStepInput = { - products: ProductDTO[] -} - -export const syncProductsStep = createStep( - "sync-products", - async ({ products }: SyncProductsStepInput, { container }) => { - const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE) - - const existingProducts = (await algoliaModuleService.retrieveFromIndex( - products.map((product) => product.id), - "product" - )).results.filter(Boolean) - const newProducts = products.filter( - (product) => !existingProducts.some((p) => p.objectID === product.id) - ) - await algoliaModuleService.indexData( - products as unknown as Record[], - "product" - ) - - return new StepResponse(undefined, { - newProducts: newProducts.map((product) => product.id), - existingProducts, - }) - } - // TODO add compensation -) -``` - -You create a step with `createStep` from the Workflows SDK. It accepts two parameters: - -1. The step's unique name, which is `sync-products`. -2. An async function that receives two parameters: - - The step's input, which is in this case an object holding an array of products to sync into Algolia. - - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. - -In the step function, you resolve the Algolia Module's service from the Medusa container using the name you exported in the module definition's file. - -Then, you retrieve the products that are already indexed in Algolia and determine which products are new. You'll learn why this is useful in a bit. - -Finally, you pass the products you received in the input to Algolia to create or update its indices. - -A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: - -1. The step's output, which in this case is `undefined`. -2. Data to pass to the step's compensation function. - -#### Compensation Function - -The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems. - -To add a compensation function to a step, pass it as a third parameter to `createStep`: - -```ts title="src/workflows/steps/sync-products.ts" -export const syncProductsStep = createStep( - // ... - async (input, { container }) => { - if (!input) { - return - } - - const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE) - - if (input.newProducts) { - await algoliaModuleService.deleteFromIndex( - input.newProducts, - "product" - ) - } - - if (input.existingProducts) { - await algoliaModuleService.indexData( - input.existingProducts, - "product" - ) - } - } -) -``` - -The compensation function receives two parameters: - -1. The data you passed as a second parameter of `StepResponse` in the step function. -2. A context object similar to the step function that holds the Medusa container. - -In the compensation function, you resolve the Algolia Module's service from the container. Then, you delete from Algolia the products that were newly indexed, and revert the existing products to their original data. - -### Add Sync Products Workflow - -You can now create the worklow that syncs the products to Algolia. - -To create the workflow, create the file `src/workflows/sync-products.ts` with the following content: - -```ts title="src/workflows/sync-products.ts" -import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" -import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products" - -type SyncProductsWorkflowInput = { - filters?: Record - limit?: number - offset?: number -} - -export const syncProductsWorkflow = createWorkflow( - "sync-products", - ({ filters, limit, offset }: SyncProductsWorkflowInput) => { - // @ts-ignore - const { data, metadata } = useQueryGraphStep({ - entity: "product", - fields: ["id", "title", "description", "handle", "thumbnail", "categories.*", "tags.*"], - pagination: { - take: limit, - skip: offset, - }, - filters: { - // @ts-ignore - status: "published", - ...filters, - }, - }) - - syncProductsStep({ - products: data, - } as SyncProductsStepInput) - - return new WorkflowResponse({ - products: data, - metadata, - }) - } -) -``` - -You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. - -It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is pagination and filter parameters for the products to retrieve. - -In the workflow's constructor function, you: - -1. Execute `useQueryGraphStep` to retrieve products from Medusa's database. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input. -2. Execute `syncProductsStep` to index the products in Algolia. You pass it the products you retrieved in the previous step. - -A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details. - -In the next step, you'll learn how to execute this workflow. - -*** - -## Step 4: Trigger Algolia Sync Manually - -As mentioned earlier, you'll trigger the Algolia sync automatically when product events occur, but you also want to allow the admin to manually trigger a reindex. - -In this step, you'll add the functionality to trigger the `syncProductsWorkflow` manually from the Medusa Admin dashboard. This requires: - -1. Creating a subscriber that listens to a custom `algolia.sync` event to trigger syncing products to Algolia. -2. Creating an API route that the Medusa Admin dashboard can call to emit the `algolia.sync` event, which triggers the subscriber. -3. Add a new page or UI route to the Medusa Admin dashboard to allow the admin to trigger the reindex. - -### Create Products Sync Subscriber - -A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background. - -Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create the subscriber that listens to the `algolia.sync` event, create the file `src/subscribers/algolia-sync.ts` with the following content: - -```ts title="src/subscribers/algolia-sync.ts" -import { - SubscriberArgs, - type SubscriberConfig, -} from "@medusajs/framework" -import { syncProductsWorkflow } from "../workflows/sync-products" - -export default async function algoliaSyncHandler({ - container, -}: SubscriberArgs) { - const logger = container.resolve("logger") - - let hasMore = true - let offset = 0 - const limit = 50 - let totalIndexed = 0 - - logger.info("Starting product indexing...") - - while (hasMore) { - const { result: { products, metadata } } = await syncProductsWorkflow(container) - .run({ - input: { - limit, - offset, - }, - }) - - hasMore = offset + limit < (metadata?.count ?? 0) - offset += limit - totalIndexed += products.length - } - - logger.info(`Successfully indexed ${totalIndexed} products`) -} - -export const config: SubscriberConfig = { - event: "algolia.sync", -} -``` - -A subscriber file must export: - -1. An asynchronous function, which is the subscriber that is executed when the event is emitted. -2. A configuration object that holds the name of the event the subscriber listens to, which is `algolia.sync` in this case. - -The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container. - -In the subscriber function, you initialize variables to keep track of the pagination and the total number of products indexed. - -Then, you start a loop that retrieves products in batches of 50 and indexes them in Algolia using the `syncProductsWorkflow`. Finally, you log the total number of products indexed. - -You'll learn how to emit the `algolia.sync` event next. - -If you want to sync other data types, you can do it in this subscriber as well. - -### Create API Route to Trigger Sync - -To allow the Medusa Admin dashboard to trigger the `algolia.sync` event, you need to create an API route that emits the event. - -An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. - -Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). - -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`. - -So, to create an API route at the path `/admin/algolia/sync`, create the file `src/api/admin/algolia/sync/route.ts` with the following content: - -```ts title="src/api/admin/algolia/sync/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { Modules } from "@medusajs/framework/utils" - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const eventModuleService = req.scope.resolve(Modules.EVENT_BUS) - await eventModuleService.emit({ - name: "algolia.sync", - data: {}, - }) - res.send({ - message: "Syncing data to Algolia", - }) -} -``` - -Since you export a `POST` route handler function, you expose an `API` route at `/admin/algolia/sync`. The route handler function accepts two parameters: - -1. A request object with details and context on the request, such as body parameters or authenticated user details. -2. A response object to manipulate and send the response. - -In the route handler, you use the Medusa container that is available in the request object to resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). This module manages events and their subscribers. - -Then, you emit the `algolia.sync` event using the Event Module's `emit` method, passing it the event name. - -Finally, you send a response with a message indicating that data is being synced to Algolia. - -### Add Algolia Sync Page to Admin Dashboard - -The last step is to add a new page to the admin dashboard that allows the admin to trigger the reindex. You add a new page using a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). - -A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked. - -Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). - -#### Configure JS SDK - -Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations. - -The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content: - -```ts title="src/admin/lib/sdk.ts" -import Medusa from "@medusajs/js-sdk" - -export const sdk = new Medusa({ - baseUrl: "http://localhost:9000", - debug: process.env.NODE_ENV === "development", - auth: { - type: "session", - }, -}) -``` - -You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties: - -- `baseUrl`: The base URL of the Medusa server. -- `debug`: A boolean indicating whether to log debug information into the console. -- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type. - -#### Create UI Route - -You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. - -So, to create a new page under the Settings section of the Medusa Admin, create the file `src/admin/routes/settings/algolia/page.tsx` with the following content: - -```tsx title="src/admin/routes/settings/algolia/page.tsx" -import { Container, Heading, Button, toast } from "@medusajs/ui" -import { useMutation } from "@tanstack/react-query" -import { sdk } from "../../../lib/sdk" -import { defineRouteConfig } from "@medusajs/admin-sdk" - -const AlgoliaPage = () => { - const { mutate, isPending } = useMutation({ - mutationFn: () => - sdk.client.fetch("/admin/algolia/sync", { - method: "POST", - }), - onSuccess: () => { - toast.success("Successfully triggered data sync to Algolia") - }, - onError: (err) => { - console.error(err) - toast.error("Failed to sync data to Algolia") - }, - }) - - const handleSync = () => { - mutate() - } - - return ( - -
- Algolia Sync -
-
- -
-
- ) -} - -export const config = defineRouteConfig({ - label: "Algolia", -}) - -export default AlgoliaPage -``` - -A UI route's file must export: - -1. A React component that defines the content of the page. -2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route. - -In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request. - -Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier. - -### Test it Out - -You'll now test out the entire flow, starting from triggering the reindex manually from the Medusa Admin dashboard, to checking the Algolia dashboard for the indexed products. - -Run the following command to start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the credentials you set up in the first step. - -Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md). - -After you log in, go to Settings from the sidebar. You'll find in the Settings' sidebar a new "Algolia" item. If you click on it, you'll find the page you created with the button to sync products to Algolia. - -If you click on the button, the products will be synced to Algolia. - -![The Algolia Sync page in the Medusa Admin dashboard with a button to sync products to Algolia](https://res.cloudinary.com/dza7lstvk/image/upload/v1742820813/Medusa%20Resources/Screenshot_2025-03-24_at_2.52.31_PM_eiegzb.png) - -You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages: - -```bash -info: Processing algolia.sync which has 1 subscribers -info: Starting product indexing... -info: Successfully indexed 4 products -``` - -These messages indicate that the `algolia.sync` event was emitted, which triggered the subscriber you created to sync the products using the `syncProductsWorkflow`. - -Finally, you can check the Algolia dashboard to see the indexed products. Go to Search -> Index, and check the records of the index you set up in the Algolia Module's options (`products`, for example). - -![The Algolia dashboard showing the indexed products](https://res.cloudinary.com/dza7lstvk/image/upload/v1742821034/Medusa%20Resources/Screenshot_2025-03-24_at_2.56.38_PM_mtojrv.png) - -*** - -## Step 5: Update Index on Product Changes - -You'll now automate the indexing of the products whenever a change occurs. That includes when a product is created, updated, or deleted. - -Similar to before, you'll create subscribers to listen to these events. - -### Handle Create and Update Products - -The action to perform when a product is created or updated is the same. You'll use the `syncProductsWorkflow` to sync the product to Algolia. - -So, you only need one subscriber to handle these two events. To create the subscriber, create the file `src/subscribers/product-sync.ts` with the following content: - -```ts title="src/subscribers/product-sync.ts" -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { syncProductsWorkflow } from "../workflows/sync-products" - -export default async function handleProductEvents({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await syncProductsWorkflow(container) - .run({ - input: { - filters: { - id: data.id, - }, - }, - }) -} - -export const config: SubscriberConfig = { - event: ["product.created", "product.updated"], -} -``` - -The subscriber listens to the `product.created` and `product.updated` events. When either of these events is emitted, the subscriber triggers the `syncProductsWorkflow` to sync the product to Algolia. - -When the `product.created` and `product.updated` events are emitted, the product's ID is passed in the event data payload, which you can access in the `event.data` property of the subscriber function's parameter. - -So, you pass the product's ID to the `syncProductsWorkflow` as a filter to retrieve only the product that was created or updated. - -#### Test it Out - -To test it out, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was created or updated. - -### Handle Product Deletion - -When a product is deleted, you need to remove it from the Algolia index. As this requires a different action than creating or updating a product, you'll create a new workflow that deletes the product from Algolia, then create a subscriber that listens to the `product.deleted` event to trigger the workflow. - -#### Create Delete Product Step - -The workflow to delete a product from Algolia will have only one step that deletes products by their IDs from Algolia. - -So, create the step at `src/workflows/steps/delete-products-from-algolia.ts` with the following content: - -```ts title="src/workflows/steps/delete-products-from-algolia.ts" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { ALGOLIA_MODULE } from "../../modules/algolia" - -export type DeleteProductsFromAlgoliaWorkflow = { - ids: string[] -} - -export const deleteProductsFromAlgoliaStep = createStep( - "delete-products-from-algolia-step", - async ( - { ids }: DeleteProductsFromAlgoliaWorkflow, - { container } - ) => { - const algoliaModuleService = container.resolve(ALGOLIA_MODULE) - - const existingRecords = await algoliaModuleService.retrieveFromIndex( - ids, - "product" - ) - await algoliaModuleService.deleteFromIndex( - ids, - "product" - ) - - return new StepResponse(undefined, existingRecords) - }, - async (existingRecords, { container }) => { - if (!existingRecords) { - return - } - const algoliaModuleService = container.resolve(ALGOLIA_MODULE) - - await algoliaModuleService.indexData( - existingRecords as unknown as Record[], - "product" - ) - } -) -``` - -The step receives the IDs of the products to delete as an input. - -In the step, you resolve the Algolia Module's service and retrieve the existing records from Algolia. This is useful to revert the deletion if an error occurs. - -Then, you delete the products from Algolia and pass the existing records to the compensation function. - -In the compensation function, you reindex the existing records if an error occurs. - -#### Create Delete Product Workflow - -You can now create the workflow that deletes products from Algolia. Create the file `src/workflows/delete-products-from-algolia.ts` with the following content: - -```ts title="src/workflows/delete-products-from-algolia.ts" -import { createWorkflow } from "@medusajs/framework/workflows-sdk" -import { deleteProductsFromAlgoliaStep } from "./steps/delete-products-from-algolia" - -type DeleteProductsFromAlgoliaWorkflowInput = { - ids: string[] -} - -export const deleteProductsFromAlgoliaWorkflow = createWorkflow( - "delete-products-from-algolia", - (input: DeleteProductsFromAlgoliaWorkflowInput) => { - deleteProductsFromAlgoliaStep(input) - } -) -``` - -The workflow receives an object with the IDs of the products to delete. It then executes the `deleteProductsFromAlgoliaStep` to delete the products from Algolia. - -#### Create Delete Product Subscriber - -Finally, you'll create the subscriber that listens to the `product.deleted` event to trigger the above workflow. - -Create the file `src/subscribers/product-delete.ts` with the following content: - -```ts title="src/subscribers/product-delete.ts" -import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" -import { deleteProductsFromAlgoliaWorkflow } from "../workflows/delete-products-from-algolia" - -export default async function handleProductDeleted({ - event: { data }, - container, -}: SubscriberArgs<{ id: string }>) { - await deleteProductsFromAlgoliaWorkflow(container) - .run({ - input: { - ids: [data.id], - }, - }) -} - -export const config: SubscriberConfig = { - event: "product.deleted", -} -``` - -The subscriber listens to the `product.deleted` event. When the event is emitted, the subscriber triggers the `deleteProductsFromAlgoliaWorkflow`, passing it the ID of the product to delete. - -#### Test it Out - -To test product deletion, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, delete a product from the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was deleted there as well. - -*** - -## Step 6: Add Search API Route - -Before customizing the storefront to show the search UI, you'll create an API route in your Medusa application that allows storefronts to search products in Algolia. - -While you can implement the search functionality directly in the storefront to interact with Algolia, this approach centralizes your search integration in Medusa, allowing you to change or modify the integration as necessary. You can also rely on the same behavior and results across different storefronts. - -To implement the API Route, create the file `src/api/store/products/search/route.ts` with the following content: - -```ts title="src/api/store/products/search/route.ts" -import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" -import { ALGOLIA_MODULE } from "../../../../modules/algolia" -import AlgoliaModuleService from "../../../../modules/algolia/service" -import { z } from "zod" - -export const SearchSchema = z.object({ - query: z.string(), -}) - -type SearchRequest = z.infer - -export async function POST( - req: MedusaRequest, - res: MedusaResponse -) { - const algoliaModuleService: AlgoliaModuleService = req.scope.resolve(ALGOLIA_MODULE) - - const { query } = req.validatedBody - - const results = await algoliaModuleService.search( - query as string - ) - - res.json(results) -} -``` - -You first define a schema with [Zod](https://zod.dev/), a library to define validation schemas. The schema defines the structure of the request body, which in this case is an object with a `query` property of type `string`. Later, you'll use the schema to enforce request body validation. - -Then, you expose a `POST` API Route at `/store/search` that searches Algolia using the `search` method you implemented in the Algolia Module's service. You pass to it the query string from the request body. - -Finally, you return the search results as it is in the response. - -### Add Validation Middleware - -To ensure that requests sent to the API route have the required request body parameters, you can use 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. It's executed before the route handler. - -Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). - -Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content: - -```ts title="src/api/middlewares.ts" -import { - defineMiddlewares, - validateAndTransformBody, -} from "@medusajs/framework/http" -import { SearchSchema } from "./store/products/search/route" - -export default defineMiddlewares({ - routes: [ - { - matcher: "/store/products/search", - method: ["POST"], - middlewares: [ - validateAndTransformBody(SearchSchema), - ], - }, - ], -}) -``` - -To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties: - -- `matcher`: The path of the route the middleware applies to. -- `method`: The HTTP methods the middleware applies to, which is in this case `POST`. -- `middlewares`: An array of middleware functions to apply to the route. You apply the `validateAndTransformBody` middleware which ensures that a request's body has the required parameters. You pass it the schema you defined earlier in the API route's file. - -You can use the search API route now. You'll see it in action as you customize the storefront in the next step. - -*** - -## Step 7: Search Products in Next.js Starter Storefront - -The last step is to provide the search functionalities to customers on your storefront. In the first step, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. - -In this step, you'll customize the Next.js Starter Storefront to add the search functionality. - -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-search`, you can find the storefront by going back to the parent directory and changing to the `medusa-search-storefront` directory: - -```bash -cd ../medusa-search-storefront # change based on your project name -``` - -### Install Algolia Packages - -Before adding the implementation of the search functionality, you need to install the Algolia packages necessary to add the search functionality in your storefront. - -Run the following command in the directory of your Next.js Starter Storefront: - -```bash npm2yarn -npm install algoliasearch react-instantsearch -``` - -This installs the Algolia Search JavaScript client and the React InstantSearch library, which you'll use to build the search functionality. - -### Add Search Client Configuration - -Next, you need to configure the search client. Not only do you need to initialize Algolia, but you also need to change the searching mechanism to use your custom API route in the Medusa application instead of Algolia's API directly. - -In `src/lib/config.ts`, add the following imports at the top of the file: - -```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue" -import { - liteClient as algoliasearch, - LiteClient as SearchClient, -} from "algoliasearch/lite" -``` - -Then, add the following at the end of the file: - -```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue" -export const searchClient: SearchClient = { - ...(algoliasearch( - process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "", - process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "" - )), - search: async (params) => { - const request = Array.isArray(params) ? params[0] : params - const query = "params" in request ? request.params?.query : - "query" in request ? request.query : "" - - if (!query) { - return { - results: [ - { - hits: [], - nbHits: 0, - nbPages: 0, - page: 0, - hitsPerPage: 0, - processingTimeMS: 0, - query: "", - params: "", - }, - ], - } - } - - return await sdk.client.fetch(`/store/products/search`, { - method: "POST", - body: { - query, - }, - }) - }, -} -``` - -In the code above, you create a `searchClient` object that initializes the Algolia client with your Algolia App ID and API Key. - -You also define a `search` method that sends a `POST` request to the search API route you created in the Medusa application. You use the JS SDK (which is initialized in this same file) to send the request. - -### Set Environment Variables - -In the storefront's `.env.local` file, add the following Algolia-related environment variables: - -```plain badgeLabel="Storefront" badgeColor="blue" -NEXT_PUBLIC_ALGOLIA_APP_ID=your_algolia_app_id -NEXT_PUBLIC_ALGOLIA_API_KEY=your_algolia_api_key -NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=your-products-index-name -``` - -Where: - -- `your_algolia_app_id` is your Algolia App ID, as explained in the [Add Environment Variables section](#add-environment-variables) earlier. -- `your_algolia_api_key` is your Algolia Search API key. You can retrieve it from the [same API keys page on the Algolia dashboard](#add-environment-variables) that you retrieved the Admin API key from. -- `your-products-index-name` is the name of the index you created in Algolia to store the products, which you can retrieve as explained in the [Add Environment Variables section](#add-environment-variables) earlier. You'll use this variable later. - -Do not expose your Admin API key in the storefront. The Admin API key should only be used in the Medusa application to interact with Algolia, as it has full access to your Algolia account. - -### Add Search Modal Component - -You'll now add a search modal component that customers can use to search for products. The search modal will display the search results in real-time as the customer types in the search query. - -Later, you'll add the search modal to the navigation bar, allowing customers to open the search modal from any page. - -Create the file `src/modules/search/components/modal/index.tsx` with the following content: - -```tsx title="src/modules/search/components/modal/index.tsx" badgeLabel="Storefront" badgeColor="blue" -"use client" - -import React, { useEffect, useState } from "react" -import { Hits, InstantSearch, SearchBox } from "react-instantsearch" -import { searchClient } from "../../../../lib/config" -import Modal from "../../../common/components/modal" -import { Button } from "@medusajs/ui" -import Image from "next/image" -import Link from "next/link" -import { usePathname } from "next/navigation" - -type Hit = { - objectID: string; - id: string; - title: string; - description: string; - handle: string; - thumbnail: string; -} - -export default function SearchModal() { - const [isOpen, setIsOpen] = useState(false) - const pathname = usePathname() - - useEffect(() => { - setIsOpen(false) - }, [pathname]) - - return ( - <> -
- -
- setIsOpen(false)}> - - - - - - - ) -} - -const Hit = ({ hit }: { hit: Hit }) => { - return ( -
- {hit.title} -
-

{hit.title}

-

{hit.description}

-
- -
- ) -} -``` - -You create a `SearchModal` component that displays a search box and the search results using widgets from Algolia's `react-instantsearch` library. - -To display each result item (or hit), you create a `Hit` component that displays the product's title, description, and thumbnail. You also add a link to the product's page. - -Finally, you show the search modal when the customer clicks a "Search" button, which you'll add to the navigation bar next. - -### Add Search Button to Navigation Bar - -The last step is to show the search button in the navigation bar. - -In `src/modules/layout/templates/nav/index.tsx`, add the following imports at the top of the file: - -```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue" -import SearchModal from "@modules/search/components/modal" -``` - -Then, in the return statement of the `Nav` component, add the `SearchModal` component before the `div` surrounding the "Account" link: - -```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue" - -``` - -The search button will now appear in the navigation bar before the Account link. - -### Test it Out - -To test out the storefront changes and the search API route, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, start the Next.js Starter Storefront from its directory: - -```bash npm2yarn -npm run dev -``` - -Next, go to `localhost:8000`. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page. - -![The Next.js Starter Storefront showing the search modal with search results](https://res.cloudinary.com/dza7lstvk/image/upload/v1742827777/Medusa%20Resources/Screenshot_2025-03-24_at_4.49.23_PM_kzhldx.png) - -*** - -## Next Steps - -You've now integrated Algolia with Medusa and added search functionality to your storefront. You can expand on these features to: - -- Add filters to the search results. You can do that using Algolia's [widgets](https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react/) and customizing the search API route in Medusa to accept filter parameters. -- Support indexing other data types, such as product categories. You can create the subscribers and workflows for categories similar to products. - -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). - - # 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. @@ -53787,6 +54642,1276 @@ If you're new to Medusa, check out the [main documentation](https://docs.medusaj To learn more about the commerce features that Medusa provides, check out Medusa's [Commerce Modules](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/index.html.md). +# Integrate Medusa with Resend (Email Notifications) + +In this guide, you'll learn how to integrate Medusa with Resend. + +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as an email service, that allow you to build your unique requirements around core commerce flows. + +[Resend](https://resend.com/docs/introduction) is an email service with an intuitive developer experience to send emails from any application type, including Node.js servers. By integrating Resend with Medusa, you can build flows to send an email when a commerce operation is performed, such as when an order is placed. + +This guide will teach you how to: + +- Install and set up Medusa. +- Integrate Resend into Medusa for sending emails. +- Build a flow to send an email with Resend when a customer places an order. + +You can follow this guide whether you're new to Medusa or an advanced Medusa developer. + +[Example Repository](https://github.com/medusajs/examples/tree/main/resend-integration): Find the full code of the guide in this repository. + +*** + +## Step 1: Install a Medusa Application + +### Prerequisites + +- [Node.js v20+](https://nodejs.org/en/download) +- [Git CLI tool](https://git-scm.com/downloads) +- [PostgreSQL](https://www.postgresql.org/download/) + +Start by installing the Medusa application on your machine with the following command: + +```bash +npx create-medusa-app@latest +``` + +You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes. + +Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js Starter Storefront in a directory with the `{project-name}-storefront` name. + +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). + +Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard. + +The Next.js Starter Storefront is also running at `http://localhost:8000`. + +Check out the [troubleshooting guides](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/troubleshooting/create-medusa-app-errors/index.html.md) for help. + +*** + +## Step 2: Prepare Resend Account + +If you don't have a Resend Account, create one on [their website](https://resend.com/emails). + +In addition, Resend allows you to send emails from the address `onboarding@resend.dev` only to your account's email, which is useful for development purposes. If you have a custom domain to send emails from, add it to your Resend account's domains: + +1. Go to Domains from the sidebar. +2. Click on Add Domain. + +![Click on Domains in the sidebar then on the Add Domain button in the middle of the page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523238/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.11_AM_pmqgtv.png) + +3\. In the form that opens, enter your domain name and select a region close to your users, then click Add. + +![A pop-up window with Domain and Region fields.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523280/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.52_AM_sw2pr4.png) + +4\. In the domain's details page that opens, you'll find DNS records to add to your DNS provider. After you add them, click on Verify DNS Records. You can start sending emails from your custom domain once it's verified. + +The steps to add DNS records are different for each provider, so refer to your provider's documentation or knowledge base for more details. + +![The DNS records to add are in a table under the DNS Records section. Once added, click the Verify DNS Records button at the top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523394/Medusa%20Resources/Screenshot_2024-11-25_at_10.20.56_AM_ktvbse.png) + +You also need an API key to connect to your Resend account from Medusa, but you'll create that one in a later section. + +*** + +## Step 3: Install Resend Dependencies + +In this step, you'll install two packages useful for your Resend integration: + +1. `resend`, which is the Resend SDK: + +```bash npm2yarn +npm install resend +``` + +2\. [react-email](https://github.com/resend/react-email), which is a package created by Resend to create email templates with React: + +```bash npm2yarn +npm install @react-email/components -E +``` + +You'll use these packages in the next steps. + +*** + +## Step 4: Create Resend Module Provider + +To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. + +Medusa's Notification Module delegates sending notifications to other modules, called module providers. In this step, you'll create a Resend Module Provider that implements sending notifications through the email channel. In later steps, you'll send email notifications with Resend when an order is placed through this provider. + +Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). + +### Create Module Directory + +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/resend`. + +### Create Service + +You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. + +In this section, you'll create the Resend Module Provider's service and the methods necessary to send an email with Resend. + +Start by creating the file `src/modules/resend/service.ts` with the following content: + +```ts title="src/modules/resend/service.ts" highlights={serviceHighlights1} +import { + AbstractNotificationProviderService, +} from "@medusajs/framework/utils" +import { + Logger, +} from "@medusajs/framework/types" +import { + Resend, +} from "resend" + +type ResendOptions = { + api_key: string + from: string + html_templates?: Record +} + +class ResendNotificationProviderService extends AbstractNotificationProviderService { + static identifier = "notification-resend" + private resendClient: Resend + private options: ResendOptions + private logger: Logger + + // ... +} + +export default ResendNotificationProviderService +``` + +A Notification Module Provider's service must extend the `AbstractNotificationProviderService`. It has a `send` method that you'll implement soon. The service must also have an `identifier` static property, which is a unique identifier that the Medusa application will use to register the provider in the database. + +The `ResendNotificationProviderService` class also has the following properties: + +- `resendClient` of type `Resend` (from the Resend SDK you installed in the previous step) to send emails through Resend. +- `options` of type `ResendOptions`. Modules accept options through Medusa's configurations. This ensures that the module is reusable across applications and you don't use sensitive variables like API keys directly in your code. The options that the Resend Module Provider accepts are: + - `api_key`: The Resend API key. + - `from`: The email address to send the emails from. + - `html_templates`: An optional object to replace the default subject and template that the Resend Module uses. This is also useful to support custom emails in different Medusa application setups. +- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. + +To send requests using the `resendClient`, you need to initialize it in the class's constructor. So, add the following constructor to `ResendNotificationProviderService`: + +```ts title="src/modules/resend/service.ts" +// ... + +type InjectedDependencies = { + logger: Logger +} + +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... + constructor( + { logger }: InjectedDependencies, + options: ResendOptions + ) { + super() + this.resendClient = new Resend(options.api_key) + this.options = options + this.logger = logger + } +} +``` + +A module's service accepts two parameters: + +1. Dependencies resolved from the [Module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is the module's local registry that the Medusa application adds Framework tools to. In this service, you resolve the [Logger utility](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the module's container. +2. The module's options that are passed to the module in Medusa's configuration as you'll see in a later section. + +Using the API key passed in the module's options, you initialize the Resend client. You also set the `options` and `logger` properties. + +#### Validate Options Method + +A Notification Module Provider's service can implement a static `validateOptions` method that ensures the options passed to the module through Medusa's configurations are valid. + +So, add to the `ResendNotificationProviderService` the `validateOptions` method: + +```ts title="src/modules/resend/service.ts" +// other imports... +import { + // other imports... + MedusaError, +} from "@medusajs/framework/utils" + +// ... + +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... + static validateOptions(options: Record) { + if (!options.api_key) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Option `api_key` is required in the provider's options." + ) + } + if (!options.from) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Option `from` is required in the provider's options." + ) + } + } +} +``` + +In the `validateOptions` method, you throw an error if the `api_key` or `from` options aren't passed to the module. To throw errors, you use `MedusaError` from the Modules SDK. This ensures errors follow Medusa's conventions and are displayed similar to Medusa's errors. + +#### Implement Template Methods + +Each email type has a different template and content. For example, order confirmation emails show the order's details, whereas customer confirmation emails show a greeting message to the customer. + +So, add two methods to the `ResendNotificationProviderService` class that retrieve the email template and subject of a specified template type: + +```ts title="src/modules/resend/service.ts" highlights={serviceHighlights2} +// imports and types... + +enum Templates { + ORDER_PLACED = "order-placed", +} + +const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { + // TODO add templates +} + +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... + getTemplate(template: Templates) { + if (this.options.html_templates?.[template]) { + return this.options.html_templates[template].content + } + const allowedTemplates = Object.keys(templates) + + if (!allowedTemplates.includes(template)) { + return null + } + + return templates[template] + } + + getTemplateSubject(template: Templates) { + if (this.options.html_templates?.[template]?.subject) { + return this.options.html_templates[template].subject + } + switch(template) { + case Templates.ORDER_PLACED: + return "Order Confirmation" + default: + return "New Email" + } + } +} +``` + +You first define a `Templates` enum, which holds the names of supported template types. You can add more template types to this enum later. You also define a `templates` variable that specifies the React template for each template type. You'll add templates to this variable later. + +In the `ResendNotificationProviderService` you add two methods: + +- `getTemplate`: Retrieve the template of a template type. If the `html_templates` option is set for the specified template type, you return its `content`'s value. Otherwise, you retrieve the template from the `templates` variable. +- `getTemplateSubject`: Retrieve the subject of a template type. If a `subject` is passed for the template type in the `html_templates`, you return its value. Otherwise, you return a subject based on the template type. + +You'll use these methods in the `send` method next. + +#### Implement Send Method + +In this section, you'll implement the `send` method of `ResendNotificationProviderService`. When you send a notification through the email channel later using the Notification Module, the Notification Module's service will use this `send` method under the hood to send the email with Resend. + +In the `send` method, you'll retrieve the template and subject of the email template, then send the email using the Resend client. + +Add the `send` method to the `ResendNotificationProviderService` class: + +```ts title="src/modules/resend/service.ts" highlights={serviceHighlights3} +// other imports... +import { + // ... + ProviderSendNotificationDTO, + ProviderSendNotificationResultsDTO, +} from "@medusajs/framework/types" +import { + // ... + CreateEmailOptions, +} from "resend" + +class ResendNotificationProviderService extends AbstractNotificationProviderService { + // ... + async send( + notification: ProviderSendNotificationDTO + ): Promise { + const template = this.getTemplate(notification.template as Templates) + + if (!template) { + this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`) + return {} + } + + const commonOptions = { + from: this.options.from, + to: [notification.to], + subject: this.getTemplateSubject(notification.template as Templates), + } + + let emailOptions: CreateEmailOptions + if (typeof template === "string") { + emailOptions = { + ...commonOptions, + html: template, + } + } else { + emailOptions = { + ...commonOptions, + react: template(notification.data), + } + } + + const { data, error } = await this.resendClient.emails.send(emailOptions) + + if (error || !data) { + if (error) { + this.logger.error("Failed to send email", error) + } else { + this.logger.error("Failed to send email: unknown error") + } + return {} + } + + return { id: data.id } + } +} +``` + +The `send` method receives the notification details object as a parameter. Some of its properties include: + +- `to`: The address to send the notification to. +- `template`: The template type of the notification. +- `data`: The data useful for the email type. For example, when sending an order-confirmation email, `data` would hold the order's details. + +In the method, you retrieve the template and subject of the email using the methods you defined earlier. Then, you put together the data to pass to Resend, such as the email address to send the notification to and the email address to send from. + +Also, if the email's template is a string, it's passed as an HTML template. Otherwise, it's passed as a React template. + +Finally, you use the `emails.send` method of the Resend client to send the email. If an error occurs you log it in the terminal. Otherwise, you return the ID of the send email as received from Resend. Medusa uses this ID when creating the notification in its database. + +### Export Module Definition + +The `ResendNotificationProviderService` class now has the methods necessary to start sending emails. + +Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service. + +Create the file `src/modules/resend/index.ts` with the following content: + +```ts title="src/modules/resend/index.ts" +import { + ModuleProvider, + Modules, +} from "@medusajs/framework/utils" +import ResendNotificationProviderService from "./service" + +export default ModuleProvider(Modules.NOTIFICATION, { + services: [ResendNotificationProviderService], +}) +``` + +You export the module provider's definition using `ModuleProvider` from the Modules SDK. It accepts as a first parameter the name of the module that this provider belongs to, which is the Notification Module. It also accepts as a second parameter an object having a `service` property indicating the provider's service. + +### Add Module to Configurations + +Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations. + +Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application. + +Add the `modules` property to the exported configurations in `medusa-config.ts`: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + { + resolve: "./src/modules/resend", + id: "resend", + options: { + channels: ["email"], + api_key: process.env.RESEND_API_KEY, + from: process.env.RESEND_FROM_EMAIL, + }, + }, + ], + }, + }, + ], +}) +``` + +In the `modules` array, you pass a module object having the following properties: + +- `resolve`: The NPM package of the Notification Module. Since the Resend Module is a Notification Module Provider, it'll be passed in the options of the Notification Module. +- `options`: An object of options to pass to the module. It has a `providers` property which is an array of module providers to register. Each module provider object has the following properties: + - `resolve`: The path to the module provider to register in the application. It can also be the name of an NPM package. + - `id`: A unique ID, which Medusa will use along with the `identifier` static property that you set earlier in the class to identify this module provider. + - `options`: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service. You must also specify the `channels` option, which indicates the channels that this provider sends notifications through. + +Some of the module's options, such as the Resend API key, are set in environment variables. So, add the following environment variables to `.env`: + +```shell +RESEND_FROM_EMAIL=onboarding@resend.dev +RESEND_API_KEY= +``` + +Where: + +- `RESEND_FROM_EMAIL`: The email to send emails from. If you've configured the custom domain as explained in [Step 2](#step-2-prepare-resend-account), change this email to an email from your custom domain. Otherwise, you can use `onboarding@resend.dev` for development purposes. +- `RESEND_API_KEY` is the API key of your Resend account. To retrieve it: + - Go to API Keys in the sidebar. + - Click on the Create API Key button. + +![Click on the API keys in the sidebar, then click on the Create API Key button at the top right](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535399/Medusa%20Resources/Screenshot_2024-11-25_at_10.22.25_AM_v4d09s.png) + +- In the form that opens, enter a name for the API key (for example, Medusa). You can keep its permissions to Full Access or change it to Sending Access. Once you're done, click Add. + +![The form to create an API key with fields for the API key's name, permissions, and domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535464/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.26_AM_g7gcuc.png) + +- A new pop-up will show with your API key hidden. Copy it before closing the pop-up, since you can't access the key again afterwards. Use its value for the `RESEND_API_KEY` environment variable. + +![Click the copy icon to copy the API key](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535791/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.43_AM_divins.png) + +Your Resend Module Provider is all set up. You'll test it out in a later section. + +*** + +## Step 5: Add Order Confirmation Template + +In this step, you'll add a React template for order confirmation emails. You'll create it using the [react-email](https://github.com/resend/react-email) package you installed earlier. You can follow the same steps for other email templates, such as for customer confirmation. + +Create the directory `src/modules/resend/emails` that will hold the email templates. Then, to add the template for order confirmation, create the file `src/modules/resend/emails/order-placed.tsx` with the following content: + +```tsx title="src/modules/resend/emails/order-placed.tsx" highlights={templateHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports" +import { + Text, + Column, + Container, + Heading, + Html, + Img, + Row, + Section, + Tailwind, + Head, + Preview, + Body, + Link, +} from "@react-email/components" +import { BigNumberValue, CustomerDTO, OrderDTO } from "@medusajs/framework/types" + +type OrderPlacedEmailProps = { + order: OrderDTO & { + customer: CustomerDTO + } + email_banner?: { + body: string + title: string + url: string + } +} + +function OrderPlacedEmailComponent({ order, email_banner }: OrderPlacedEmailProps) { + const shouldDisplayBanner = email_banner && "title" in email_banner + + const formatter = new Intl.NumberFormat([], { + style: "currency", + currencyDisplay: "narrowSymbol", + currency: order.currency_code, + }) + + const formatPrice = (price: BigNumberValue) => { + if (typeof price === "number") { + return formatter.format(price) + } + + if (typeof price === "string") { + return formatter.format(parseFloat(price)) + } + + return price?.toString() || "" + } + + return ( + + + + Thank you for your order from Medusa + + {/* Header */} +
+ +
+ + {/* Thank You Message */} + + + Thank you for your order, {order.customer?.first_name || order.shipping_address?.first_name} + + + We're processing your order and will notify you when it ships. + + + + {/* Promotional Banner */} + {shouldDisplayBanner && ( + +
+ + + + {email_banner.title} + + {email_banner.body} + + + + Shop Now + + + +
+
+ )} + + {/* Order Items */} + + + Your Items + + + + Order ID: #{order.display_id} + + + {order.items?.map((item) => ( +
+ + + {item.product_title + + + + {item.product_title} + + {item.variant_title} + + {formatPrice(item.total)} + + + +
+ ))} + + {/* Order Summary */} +
+ + Order Summary + + + + Subtotal + + + + {formatPrice(order.item_total)} + + + + {order.shipping_methods?.map((method) => ( + + + {method.name} + + + {formatPrice(method.total)} + + + ))} + + + Tax + + + {formatPrice(order.tax_total || 0)} + + + + + Total + + + {formatPrice(order.total)} + + +
+
+ + {/* Footer */} +
+ + If you have any questions, reply to this email or contact our support team at support@medusajs.com. + + + Order Token: {order.id} + + + © {new Date().getFullYear()} Medusajs, Inc. All rights reserved. + +
+ + +
+ ) +} + +export const orderPlacedEmail = (props: OrderPlacedEmailProps) => ( + +) +``` + +You define the `OrderPlacedEmailComponent` which is a React email template that shows the order's details, such as items and their totals. The component accepts an `order` object as a prop. + +You also export an `orderPlacedEmail` function, which accepts props as an input and returns the `OrderPlacedEmailComponent` passing it the props. Because you can't use JSX syntax in `src/modules/resend/service.ts`, you'll import this function instead. + +Next, update the `templates` variable in `src/modules/resend/service.ts` to assign this template to the `order-placed` template type: + +```ts title="src/modules/resend/service.ts" +// other imports... +import { orderPlacedEmail } from "./emails/order-placed" + +const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { + [Templates.ORDER_PLACED]: orderPlacedEmail, +} +``` + +The `ResendNotificationProviderService` will now use the `OrderPlacedEmailComponent` as the template of order confirmation emails. + +### Test Email Out + +You'll later test out sending the email when an order is placed. However, you can also test out how the email looks like using [React Email's CLI tool](https://react.email/docs/cli). + +First, install the CLI tool in your Medusa application: + +```bash npm2yarn +npm install -D react-email +``` + +Then, in `src/modules/resend/emails/order-placed.tsx`, add the following at the end of the file: + +```tsx title="src/modules/resend/emails/order-placed.tsx" +const mockOrder = { + "order": { + "id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", + "display_id": 1, + "email": "afsaf@gmail.com", + "currency_code": "eur", + "total": 20, + "subtotal": 20, + "discount_total": 0, + "shipping_total": 10, + "tax_total": 0, + "item_subtotal": 10, + "item_total": 10, + "item_tax_total": 0, + "customer_id": "cus_01JSNXD6VQC1YH56E4TGC81NWX", + "items": [ + { + "id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9", + "title": "L", + "subtitle": "Medusa Sweatshirt", + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "variant_id": "variant_01JSNXAQCZ5X81A3NRSVFJ3ZHQ", + "product_id": "prod_01JSNXAQBQ6MFV5VHKN420NXQW", + "product_title": "Medusa Sweatshirt", + "product_description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "product_subtitle": null, + "product_type": null, + "product_type_id": null, + "product_collection": null, + "product_handle": "sweatshirt", + "variant_sku": "SWEATSHIRT-L", + "variant_barcode": null, + "variant_title": "L", + "variant_option_values": null, + "requires_shipping": true, + "is_giftcard": false, + "is_discountable": true, + "is_tax_inclusive": false, + "is_custom_price": false, + "metadata": {}, + "raw_compare_at_unit_price": null, + "raw_unit_price": { + "value": "10", + "precision": 20, + }, + "created_at": new Date(), + "updated_at": new Date(), + "deleted_at": null, + "tax_lines": [], + "adjustments": [], + "compare_at_unit_price": null, + "unit_price": 10, + "quantity": 1, + "raw_quantity": { + "value": "1", + "precision": 20, + }, + "detail": { + "id": "orditem_01JSNXDH9DK1XMESEZPADYFWKY", + "version": 1, + "metadata": null, + "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", + "raw_unit_price": null, + "raw_compare_at_unit_price": null, + "raw_quantity": { + "value": "1", + "precision": 20, + }, + "raw_fulfilled_quantity": { + "value": "0", + "precision": 20, + }, + "raw_delivered_quantity": { + "value": "0", + "precision": 20, + }, + "raw_shipped_quantity": { + "value": "0", + "precision": 20, + }, + "raw_return_requested_quantity": { + "value": "0", + "precision": 20, + }, + "raw_return_received_quantity": { + "value": "0", + "precision": 20, + }, + "raw_return_dismissed_quantity": { + "value": "0", + "precision": 20, + }, + "raw_written_off_quantity": { + "value": "0", + "precision": 20, + }, + "created_at": new Date(), + "updated_at": new Date(), + "deleted_at": null, + "item_id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9", + "unit_price": null, + "compare_at_unit_price": null, + "quantity": 1, + "fulfilled_quantity": 0, + "delivered_quantity": 0, + "shipped_quantity": 0, + "return_requested_quantity": 0, + "return_received_quantity": 0, + "return_dismissed_quantity": 0, + "written_off_quantity": 0, + }, + "subtotal": 10, + "total": 10, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + "refundable_total_per_unit": 10, + "refundable_total": 10, + "fulfilled_total": 0, + "shipped_total": 0, + "return_requested_total": 0, + "return_received_total": 0, + "return_dismissed_total": 0, + "write_off_total": 0, + "raw_subtotal": { + "value": "10", + "precision": 20, + }, + "raw_total": { + "value": "10", + "precision": 20, + }, + "raw_original_total": { + "value": "10", + "precision": 20, + }, + "raw_discount_total": { + "value": "0", + "precision": 20, + }, + "raw_discount_subtotal": { + "value": "0", + "precision": 20, + }, + "raw_discount_tax_total": { + "value": "0", + "precision": 20, + }, + "raw_tax_total": { + "value": "0", + "precision": 20, + }, + "raw_original_tax_total": { + "value": "0", + "precision": 20, + }, + "raw_refundable_total_per_unit": { + "value": "10", + "precision": 20, + }, + "raw_refundable_total": { + "value": "10", + "precision": 20, + }, + "raw_fulfilled_total": { + "value": "0", + "precision": 20, + }, + "raw_shipped_total": { + "value": "0", + "precision": 20, + }, + "raw_return_requested_total": { + "value": "0", + "precision": 20, + }, + "raw_return_received_total": { + "value": "0", + "precision": 20, + }, + "raw_return_dismissed_total": { + "value": "0", + "precision": 20, + }, + "raw_write_off_total": { + "value": "0", + "precision": 20, + }, + }, + ], + "shipping_address": { + "id": "caaddr_01JSNXD6W0TGPH2JQD18K97B25", + "customer_id": null, + "company": "", + "first_name": "safasf", + "last_name": "asfaf", + "address_1": "asfasf", + "address_2": "", + "city": "asfasf", + "country_code": "dk", + "province": "", + "postal_code": "asfasf", + "phone": "", + "metadata": null, + "created_at": "2025-04-25T07:25:48.801Z", + "updated_at": "2025-04-25T07:25:48.801Z", + "deleted_at": null, + }, + "billing_address": { + "id": "caaddr_01JSNXD6W0V7RNZH63CPG26K5W", + "customer_id": null, + "company": "", + "first_name": "safasf", + "last_name": "asfaf", + "address_1": "asfasf", + "address_2": "", + "city": "asfasf", + "country_code": "dk", + "province": "", + "postal_code": "asfasf", + "phone": "", + "metadata": null, + "created_at": "2025-04-25T07:25:48.801Z", + "updated_at": "2025-04-25T07:25:48.801Z", + "deleted_at": null, + }, + "shipping_methods": [ + { + "id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1", + "name": "Standard Shipping", + "description": null, + "is_tax_inclusive": false, + "is_custom_amount": false, + "shipping_option_id": "so_01JSNXAQA64APG6BNHGCMCTN6V", + "data": {}, + "metadata": null, + "raw_amount": { + "value": "10", + "precision": 20, + }, + "created_at": new Date(), + "updated_at": new Date(), + "deleted_at": null, + "tax_lines": [], + "adjustments": [], + "amount": 10, + "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", + "detail": { + "id": "ordspmv_01JSNXDH9B5RAF4FH3M1HH3TEA", + "version": 1, + "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", + "return_id": null, + "exchange_id": null, + "claim_id": null, + "created_at": new Date(), + "updated_at": new Date(), + "deleted_at": null, + "shipping_method_id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1", + }, + "subtotal": 10, + "total": 10, + "original_total": 10, + "discount_total": 0, + "discount_subtotal": 0, + "discount_tax_total": 0, + "tax_total": 0, + "original_tax_total": 0, + "raw_subtotal": { + "value": "10", + "precision": 20, + }, + "raw_total": { + "value": "10", + "precision": 20, + }, + "raw_original_total": { + "value": "10", + "precision": 20, + }, + "raw_discount_total": { + "value": "0", + "precision": 20, + }, + "raw_discount_subtotal": { + "value": "0", + "precision": 20, + }, + "raw_discount_tax_total": { + "value": "0", + "precision": 20, + }, + "raw_tax_total": { + "value": "0", + "precision": 20, + }, + "raw_original_tax_total": { + "value": "0", + "precision": 20, + }, + }, + ], + "customer": { + "id": "cus_01JSNXD6VQC1YH56E4TGC81NWX", + "company_name": null, + "first_name": null, + "last_name": null, + "email": "afsaf@gmail.com", + "phone": null, + "has_account": false, + "metadata": null, + "created_by": null, + "created_at": "2025-04-25T07:25:48.791Z", + "updated_at": "2025-04-25T07:25:48.791Z", + "deleted_at": null, + }, + }, +} +// @ts-ignore +export default () => +``` + +You create a mock order object that contains the order's details. Then, you export a default function that returns the `OrderPlacedEmailComponent` passing it the mock order. + +The React Email CLI tool will use the function to render the email template. + +Finally, add the following script to `package.json`: + +```json +{ + "scripts": { + "dev:email": "email dev --dir ./src/modules/resend/emails" + } +} +``` + +This script will run the React Email CLI tool, passing it the directory where the email templates are located. + +You can now test out the email template by running the following command: + +```bash npm2yarn +npm run dev:email +``` + +This will start a development server at `http://localhost:3000`. If you open this URL, you can view your email templates in the browser. + +You can make changes to the email template, and the server will automatically reload the changes. + +![The email template rendered in the browser](https://res.cloudinary.com/dza7lstvk/image/upload/v1745568201/Medusa%20Resources/Screenshot_2025-04-25_at_10.41.26_AM_u86abc.png) + +*** + +## Step 6: Send Email when Order is Placed + +Medusa has an event system that emits an event when a commerce operation is performed. You can then listen and handle that event in an asynchronous function called a subscriber. + +So, to send a confirmation email when a customer places an order, which is a commerce operation that Medusa already implements, you don't need to extend or hack your way into Medusa's implementation as you would do with other commerce platforms. + +Instead, you'll create a subscriber that listens to the `order.placed` event and sends an email when the event is emitted. + +Learn more about Medusa's event system in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +### Send Order Confirmation Email Workflow + +To send the order confirmation email, you need to retrieve the order's details first, then use the Notification Module's service to send the email. To implement this flow, you'll create 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 a subscriber. + +Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) + +#### Send Notification Step + +You'll start by implementing the step of the workflow that sends the notification. To do that, create the file `src/workflows/steps/send-notification.ts` with the following content: + +```ts title="src/workflows/steps/send-notification.ts" +import { Modules } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { CreateNotificationDTO } from "@medusajs/framework/types" + +export const sendNotificationStep = createStep( + "send-notification", + async (data: CreateNotificationDTO[], { container }) => { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + const notification = await notificationModuleService.createNotifications(data) + return new StepResponse(notification) + } +) +``` + +You define the `sendNotificationStep` using the `createStep` function that accepts two parameters: + +- A string indicating the step's unique name. +- The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second. + +The `container` property in the second parameter is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools, such as a module's service, that you can resolve to utilize their functionalities. + +The Medusa container is accessible by all customizations, such as workflows and subscribers, except for modules. Each module has its own container with Framework tools like the Logger utility. + +In the step function, you resolve the Notification Module's service, and use its `createNotifications` method, passing it the notification's data that the step receives as an input. + +The step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a parameter the data to return to the workflow that executed this step. + +#### Workflow Implementation + +You'll now create the workflow that uses the `sendNotificationStep` to send the order confirmation email. + +Create the file `src/workflows/send-order-confirmation.ts` with the following content: + +```ts title="src/workflows/send-order-confirmation.ts" highlights={workflowHighlights} +import { + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" +import { sendNotificationStep } from "./steps/send-notification" + +type WorkflowInput = { + id: string +} + +export const sendOrderConfirmationWorkflow = createWorkflow( + "send-order-confirmation", + ({ id }: WorkflowInput) => { + // @ts-ignore + const { data: orders } = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "display_id", + "email", + "currency_code", + "total", + "items.*", + "shipping_address.*", + "billing_address.*", + "shipping_methods.*", + "customer.*", + "total", + "subtotal", + "discount_total", + "shipping_total", + "tax_total", + "item_subtotal", + "item_total", + "item_tax_total", + ], + filters: { + id, + }, + }) + + const notification = sendNotificationStep([{ + to: orders[0].email, + channel: "email", + template: "order-placed", + data: { + order: orders[0], + }, + }]) + + return new WorkflowResponse(notification) + } +) +``` + +You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. + +It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps: + +1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details. +2. `sendNotificationStep` which is the step you implemented. You pass it an array with one object, which is the notification's details having following properties: + - `to`: The address to send the email to. You pass the customer's email that is stored in the order. + - `channel`: The channel to send the notification through, which is `email`. Since you specified `email` in the Resend Module Provider's `channel` option, the Notification Module will delegate the sending to the Resend Module Provider's service. + - `template`: The email's template type. You retrieve the template content in the `ResendNotificationProviderService`'s `send` method based on the template specified here. + - `data`: The data to pass to the email template, which is the order's details. + +A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). + +You'll execute the workflow when you create the subscriber next. + +#### Add the Order Placed Subscriber + +Now that you have the workflow to send an order-confirmation email, you'll execute it in a subscriber that's executed whenever an order is placed. + +You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/order-placed.ts` with the following content: + +```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights} +import type { + SubscriberArgs, + SubscriberConfig, +} from "@medusajs/framework" +import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" + +export default async function orderPlacedHandler({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await sendOrderConfirmationWorkflow(container) + .run({ + input: { + id: data.id, + }, + }) +} + +export const config: SubscriberConfig = { + event: "order.placed", +} +``` + +A subscriber file exports: + +- An asynchronous function that's executed whenever the associated event is emitted, which is the `order.placed` event. +- A configuration object with an `event` property indicating the event the subscriber is listening to. + +The subscriber function accepts the event's details as a first paramter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). + +In the function, you execute the `sendOrderConfirmationWorkflow` by invoking it, passing it the `container`, then using its `run` method. The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload. + +This subscriber now runs whenever an order is placed. You'll see this in action in the next section. + +*** + +## Test it Out: Place an Order + +To test out the Resend integration, you'll place an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed as part of installing Medusa. + +Start your Medusa application first: + +```bash npm2yarn +npm run dev +``` + +Then, in the Next.js Starter Storefront's directory (which was installed in a directory outside of the Medusa application's directory with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory), run the following command to start the storefront: + +```bash npm2yarn +npm run dev +``` + +Then, open the storefront in your browser at `http://localhost:8000` and: + +1. Go to Menu -> Store. + +![Choose Store from Menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539139/Medusa%20Resources/Screenshot_2024-11-25_at_2.51.59_PM_fubiwj.png) + +2\. Click on a product, select its options, and add it to the cart. + +![Choose an option, such as size, then click on the Add to cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539227/Medusa%20Resources/Screenshot_2024-11-25_at_2.53.11_PM_iswcjy.png) + +3\. Click on Cart at the top right, then click Go to Cart. + +![Cart is at the top right. It opens a dropdown with a Go to Cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539354/Medusa%20Resources/Screenshot_2024-11-25_at_2.54.44_PM_b1pnlu.png) + +4\. On the cart's page, click on the "Go to checkout" button. + +![The Go to checkout button is at the right side of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539443/Medusa%20Resources/Screenshot_2024-11-25_at_2.56.27_PM_cvqshj.png) + +5\. On the checkout page, when entering the shipping address, make sure to set the email to your Resend account's email if you didn't set up a custom domain. + +![Enter your Resend account email if you didn't set up a custom domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539536/Medusa%20Resources/Screenshot_2024-11-25_at_2.58.31_PM_wmlh60.png) + +6\. After entering the shipping address, choose a delivery and payment methods, then click the Place Order button. + +Once the order is placed, you'll find the following message logged in the Medusa application's terminal: + +```bash +info: Processing order.placed which has 1 subscribers +``` + +This indicates that the `order.placed` event was emitted and its subscriber, which you added in the previous step, is executed. + +If you check the inbox of the email address you specified in the shipping address, you'll find a new email with the order's details. + +![Example of order-confirmation email](https://res.cloudinary.com/dza7lstvk/image/upload/v1732551372/Medusa%20Resources/Screenshot_2024-11-25_at_6.15.59_PM_efyuoj.png) + +*** + +## Next Steps + +You've now integrated Medusa with Resend. You can add more templates for other emails, such as customer registration confirmation, user invites, and more. Check out the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a list of all events that the Medusa application emits. + +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. @@ -56556,23 +58681,29 @@ If you encounter issues not covered in the troubleshooting guides: 3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. -# Integrate Medusa with Resend (Email Notifications) +# Integrate Algolia (Search) with Medusa -In this guide, you'll learn how to integrate Medusa with Resend. +In this tutorial, you'll learn how to integrate Medusa with Algolia. -When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as an email service, that allow you to build your unique requirements around core commerce flows. +When you install a Medusa application, you get a fully-fledged commerce platform with a Framework for customization. Medusa's architecture supports integrating third-party services, such as a search engine, allowing you to build your unique requirements around core commerce flows. -[Resend](https://resend.com/docs/introduction) is an email service with an intuitive developer experience to send emails from any application type, including Node.js servers. By integrating Resend with Medusa, you can build flows to send an email when a commerce operation is performed, such as when an order is placed. - -This guide will teach you how to: - -- Install and set up Medusa. -- Integrate Resend into Medusa for sending emails. -- Build a flow to send an email with Resend when a customer places an order. +[Algolia](https://www.algolia.com/doc/) is a search engine that enables you to build and manage an intuitive search experience for your customers. By integrating Algolia with Medusa, you can index e-commerce data, such as products, and allow clients to search through them. You can follow this guide whether you're new to Medusa or an advanced Medusa developer. -[Example Repository](https://github.com/medusajs/examples/tree/main/resend-integration): Find the full code of the guide in this repository. +## Summary + +By following this tutorial, you'll learn how to: + +- Install and set up Medusa. +- Integrate Algolia into Medusa. +- Trigger Algolia reindexing when a product is created, updated, deleted, or when the admin manually triggers a reindex. +- Customize the Next.js Starter Storefront to allow searching for products through Algolia. + +![Diagram illustrating the integration of Algolia with Medusa](https://res.cloudinary.com/dza7lstvk/image/upload/v1742889842/Medusa%20Resources/algolia-summary_lhegrr.jpg) + +- [Algolia Integration Repository](https://github.com/medusajs/examples/tree/main/algolia-integration): Find the full code for this guide in this repository. +- [OpenApi Specs for Postman](https://res.cloudinary.com/dza7lstvk/raw/upload/v1742829748/OpenApi/Algolia-Search_t1zlkd.yaml): Import this OpenApi Specs file into tools like Postman. *** @@ -56590,1236 +58721,1176 @@ Start by installing the Medusa application on your machine with the following co npx create-medusa-app@latest ``` -You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js Starter Storefront, choose `Y` for yes. +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 directory with the `{project-name}-storefront` name. +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 named `{project-name}-storefront`. -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). Learn more about Medusa's architecture in [this documentation](https://docs.medusajs.com/docs/learn/introduction/architecture/index.html.md). +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 credential and submit the form. Afterwards, you can login with the new user and explore the dashboard. - -The Next.js Starter Storefront is also running at `http://localhost:8000`. +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: Prepare Resend Account +## Step 2: Create Algolia Module -If you don't have a Resend Account, create one on [their website](https://resend.com/emails). +To integrate third-party services into 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 addition, Resend allows you to send emails from the address `onboarding@resend.dev` only to your account's email, which is useful for development purposes. If you have a custom domain to send emails from, add it to your Resend account's domains: +In this step, you'll create a custom module that provides the necessary functionalities to integrate Algolia with Medusa. -1. Go to Domains from the sidebar. -2. Click on Add Domain. +Refer to the [Modules documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md) to learn more. -![Click on Domains in the sidebar then on the Add Domain button in the middle of the page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523238/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.11_AM_pmqgtv.png) - -3\. In the form that opens, enter your domain name and select a region close to your users, then click Add. - -![A pop-up window with Domain and Region fields.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523280/Medusa%20Resources/Screenshot_2024-11-25_at_10.18.52_AM_sw2pr4.png) - -4\. In the domain's details page that opens, you'll find DNS records to add to your DNS provider. After you add them, click on Verify DNS Records. You can start sending emails from your custom domain once it's verified. - -The steps to add DNS records are different for each provider, so refer to your provider's documentation or knowledge base for more details. - -![The DNS records to add are in a table under the DNS Records section. Once added, click the Verify DNS Records button at the top right.](https://res.cloudinary.com/dza7lstvk/image/upload/v1732523394/Medusa%20Resources/Screenshot_2024-11-25_at_10.20.56_AM_ktvbse.png) - -You also need an API key to connect to your Resend account from Medusa, but you'll create that one in a later section. - -*** - -## Step 3: Install Resend Dependencies - -In this step, you'll install two packages useful for your Resend integration: - -1. `resend`, which is the Resend SDK: +Before building the module, you need to install Algolia's JavaScript client. Run the following command in your Medusa application's root directory: ```bash npm2yarn -npm install resend +npm install algoliasearch ``` -2\. [react-email](https://github.com/resend/react-email), which is a package created by Resend to create email templates with React: - -```bash npm2yarn -npm install @react-email/components -E -``` - -You'll use these packages in the next steps. - -*** - -## Step 4: Create Resend Module Provider - -To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup. - -Medusa's Notification Module delegates sending notifications to other modules, called module providers. In this step, you'll create a Resend Module Provider that implements sending notifications through the email channel. In later steps, you'll send email notifications with Resend when an order is placed through this provider. - -Learn more about modules in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/index.html.md). - ### Create Module Directory -A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/resend`. +A module is created under the `src/modules` directory of your Medusa application. So, create the directory `src/modules/algolia`. ### Create Service You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. -In this section, you'll create the Resend Module Provider's service and the methods necessary to send an email with Resend. +In this section, you'll create the Algolia Module's service and the methods necessary to manage indexed products in Algolia and search through them. -Start by creating the file `src/modules/resend/service.ts` with the following content: +To create the Algolia Module's service, create the file `src/modules/algolia/service.ts` with the following content: -```ts title="src/modules/resend/service.ts" highlights={serviceHighlights1} -import { - AbstractNotificationProviderService, -} from "@medusajs/framework/utils" -import { - Logger, -} from "@medusajs/framework/types" -import { - Resend, -} from "resend" +```ts title="src/modules/algolia/service.ts" +import { algoliasearch, SearchClient } from "algoliasearch" -type ResendOptions = { - api_key: string - from: string - html_templates?: Record +type AlgoliaOptions = { + apiKey: string; + appId: string; + productIndexName: string; } -class ResendNotificationProviderService extends AbstractNotificationProviderService { - static identifier = "notification-resend" - private resendClient: Resend - private options: ResendOptions - private logger: Logger +export type AlgoliaIndexType = "product" - // ... -} +export default class AlgoliaModuleService { + private client: SearchClient + private options: AlgoliaOptions -export default ResendNotificationProviderService -``` - -A Notification Module Provider's service must extend the `AbstractNotificationProviderService`. It has a `send` method that you'll implement soon. The service must also have an `identifier` static property, which is a unique identifier that the Medusa application will use to register the provider in the database. - -The `ResendNotificationProviderService` class also has the following properties: - -- `resendClient` of type `Resend` (from the Resend SDK you installed in the previous step) to send emails through Resend. -- `options` of type `ResendOptions`. Modules accept options through Medusa's configurations. This ensures that the module is reusable across applications and you don't use sensitive variables like API keys directly in your code. The options that the Resend Module Provider accepts are: - - `api_key`: The Resend API key. - - `from`: The email address to send the emails from. - - `html_templates`: An optional object to replace the default subject and template that the Resend Module uses. This is also useful to support custom emails in different Medusa application setups. -- `logger` property, which is an instance of Medusa's [Logger](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md), to log messages. - -To send requests using the `resendClient`, you need to initialize it in the class's constructor. So, add the following constructor to `ResendNotificationProviderService`: - -```ts title="src/modules/resend/service.ts" -// ... - -type InjectedDependencies = { - logger: Logger -} - -class ResendNotificationProviderService extends AbstractNotificationProviderService { - // ... - constructor( - { logger }: InjectedDependencies, - options: ResendOptions - ) { - super() - this.resendClient = new Resend(options.api_key) + constructor({}, options: AlgoliaOptions) { + this.client = algoliasearch(options.appId, options.apiKey) this.options = options - this.logger = logger } + + // TODO add methods } ``` -A module's service accepts two parameters: +You export a class that will be the Algolia Module's main service. In the service, you define two properties: -1. Dependencies resolved from the [Module's container](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md), which is the module's local registry that the Medusa application adds Framework tools to. In this service, you resolve the [Logger utility](https://docs.medusajs.com/docs/learn/debugging-and-testing/logging/index.html.md) from the module's container. -2. The module's options that are passed to the module in Medusa's configuration as you'll see in a later section. +- `client`: An instance of the Algolia Search Client, which you'll use to perform actions with Algolia's API. +- `options`: An object of options that the Module receives when it's registered, which you'll learn about later. The options contain: + - `apiKey`: The Algolia API key. + - `appId`: The Algolia App ID. + - `productIndexName`: The name of the index where products are stored. -Using the API key passed in the module's options, you initialize the Resend client. You also set the `options` and `logger` properties. +If you want to index other types of data, such as product categories, you can add new properties for their index names in the `AlgoliaOptions` type. -#### Validate Options Method +A module's service receives the module's options as a second parameter in its constructor. In the constructor, you initialize the Algolia client using the module's options. -A Notification Module Provider's service can implement a static `validateOptions` method that ensures the options passed to the module through Medusa's configurations are valid. +A module has a container that holds all resources registered in that module, and you can access those resources in the first parameter of the constructor. Learn more about it in the [Module Container documentation](https://docs.medusajs.com/docs/learn/fundamentals/modules/container/index.html.md). -So, add to the `ResendNotificationProviderService` the `validateOptions` method: +#### Index Data Method -```ts title="src/modules/resend/service.ts" -// other imports... -import { - // other imports... - MedusaError, -} from "@medusajs/framework/utils" +The first method you need to add to the servie is a method that receives an array of data to add or update in Algolia's index. -// ... +Add the following methods to the `AlgoliaModuleService` class: -class ResendNotificationProviderService extends AbstractNotificationProviderService { +```ts title="src/modules/algolia/service.ts" +export default class AlgoliaModuleService { // ... - static validateOptions(options: Record) { - if (!options.api_key) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Option `api_key` is required in the provider's options." - ) - } - if (!options.from) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - "Option `from` is required in the provider's options." - ) - } - } -} -``` - -In the `validateOptions` method, you throw an error if the `api_key` or `from` options aren't passed to the module. To throw errors, you use `MedusaError` from the Modules SDK. This ensures errors follow Medusa's conventions and are displayed similar to Medusa's errors. - -#### Implement Template Methods - -Each email type has a different template and content. For example, order confirmation emails show the order's details, whereas customer confirmation emails show a greeting message to the customer. - -So, add two methods to the `ResendNotificationProviderService` class that retrieve the email template and subject of a specified template type: - -```ts title="src/modules/resend/service.ts" highlights={serviceHighlights2} -// imports and types... - -enum Templates { - ORDER_PLACED = "order-placed", -} - -const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { - // TODO add templates -} - -class ResendNotificationProviderService extends AbstractNotificationProviderService { - // ... - getTemplate(template: Templates) { - if (this.options.html_templates?.[template]) { - return this.options.html_templates[template].content - } - const allowedTemplates = Object.keys(templates) - - if (!allowedTemplates.includes(template)) { - return null - } - - return templates[template] - } - - getTemplateSubject(template: Templates) { - if (this.options.html_templates?.[template]?.subject) { - return this.options.html_templates[template].subject - } - switch(template) { - case Templates.ORDER_PLACED: - return "Order Confirmation" + async getIndexName(type: AlgoliaIndexType) { + switch (type) { + case "product": + return this.options.productIndexName default: - return "New Email" + throw new Error(`Invalid index type: ${type}`) } } + + async indexData(data: Record[], type: AlgoliaIndexType = "product") { + const indexName = await this.getIndexName(type) + this.client.saveObjects({ + indexName, + objects: data.map((item) => ({ + ...item, + // set the object ID to allow updating later + objectID: item.id, + })), + }) + } } ``` -You first define a `Templates` enum, which holds the names of supported template types. You can add more template types to this enum later. You also define a `templates` variable that specifies the React template for each template type. You'll add templates to this variable later. +You define two methods: -In the `ResendNotificationProviderService` you add two methods: +1. `getIndexName`: A method that receives an `AlgoliaIndexType` (defined in the previous snippt) and returns the index name for that type. In this case, you only have one type, `product`, so you return the product index name. + - If you want to index other types of data, you can add more cases to the switch statement. +2. `indexData`: A method that receives an array of data and an `AlgoliaIndexType`. The method indexes the data in the Algolia index for the given type. + - Notice that you set the `objectID` property of each object to the object's `id`. This ensures that you later update the object instead of creating a new one. -- `getTemplate`: Retrieve the template of a template type. If the `html_templates` option is set for the specified template type, you return its `content`'s value. Otherwise, you retrieve the template from the `templates` variable. -- `getTemplateSubject`: Retrieve the subject of a template type. If a `subject` is passed for the template type in the `html_templates`, you return its value. Otherwise, you return a subject based on the template type. +#### Retrieve and Delete Methods -You'll use these methods in the `send` method next. +The next methods you'll add to the service are methods to retrieve and delete data from the Algolia index. You'll see their use later as you keep the Algolia index in sync with Medusa. -#### Implement Send Method +Add the following methods to the `AlgoliaModuleService` class: -In this section, you'll implement the `send` method of `ResendNotificationProviderService`. When you send a notification through the email channel later using the Notification Module, the Notification Module's service will use this `send` method under the hood to send the email with Resend. - -In the `send` method, you'll retrieve the template and subject of the email template, then send the email using the Resend client. - -Add the `send` method to the `ResendNotificationProviderService` class: - -```ts title="src/modules/resend/service.ts" highlights={serviceHighlights3} -// other imports... -import { +```ts title="src/modules/algolia/service.ts" +export default class AlgoliaModuleService { // ... - ProviderSendNotificationDTO, - ProviderSendNotificationResultsDTO, -} from "@medusajs/framework/types" -import { - // ... - CreateEmailOptions, -} from "resend" -class ResendNotificationProviderService extends AbstractNotificationProviderService { - // ... - async send( - notification: ProviderSendNotificationDTO - ): Promise { - const template = this.getTemplate(notification.template as Templates) + async retrieveFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") { + const indexName = await this.getIndexName(type) + return await this.client.getObjects>({ + requests: objectIDs.map((objectID) => ({ + indexName, + objectID, + })), + }) + } - if (!template) { - this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`) - return {} - } - - const commonOptions = { - from: this.options.from, - to: [notification.to], - subject: this.getTemplateSubject(notification.template as Templates), - } - - let emailOptions: CreateEmailOptions - if (typeof template === "string") { - emailOptions = { - ...commonOptions, - html: template, - } - } else { - emailOptions = { - ...commonOptions, - react: template(notification.data), - } - } - - const { data, error } = await this.resendClient.emails.send(emailOptions) - - if (error || !data) { - if (error) { - this.logger.error("Failed to send email", error) - } else { - this.logger.error("Failed to send email: unknown error") - } - return {} - } - - return { id: data.id } + async deleteFromIndex(objectIDs: string[], type: AlgoliaIndexType = "product") { + const indexName = await this.getIndexName(type) + await this.client.deleteObjects({ + indexName, + objectIDs, + }) } } ``` -The `send` method receives the notification details object as a parameter. Some of its properties include: +You define two methods: -- `to`: The address to send the notification to. -- `template`: The template type of the notification. -- `data`: The data useful for the email type. For example, when sending an order-confirmation email, `data` would hold the order's details. +1. `retrieveFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method retrieves the objects with the given IDs from the Algolia index. +2. `deleteFromIndex`: A method that receives an array of object IDs and an `AlgoliaIndexType`. The method deletes the objects with the given IDs from the Algolia index. -In the method, you retrieve the template and subject of the email using the methods you defined earlier. Then, you put together the data to pass to Resend, such as the email address to send the notification to and the email address to send from. +#### Search Method -Also, if the email's template is a string, it's passed as an HTML template. Otherwise, it's passed as a React template. +The last method you'll implement is a method to search through the Algolia index. You'll later use this method to expose the search functionality to clients, such as the Next.js Starter Storefront. -Finally, you use the `emails.send` method of the Resend client to send the email. If an error occurs you log it in the terminal. Otherwise, you return the ID of the send email as received from Resend. Medusa uses this ID when creating the notification in its database. +Add the following method to the `AlgoliaModuleService` class: + +```ts title="src/modules/algolia/service.ts" +export default class AlgoliaModuleService { + // ... + + async search(query: string, type: AlgoliaIndexType = "product") { + const indexName = await this.getIndexName(type) + return await this.client.search({ + requests: [ + { + indexName, + query, + }, + ], + }) + } +} +``` + +The `search` method receives a query string and an `AlgoliaIndexType`. The method searches through the Algolia index for the given type, such as products, and returns the results. ### Export Module Definition -The `ResendNotificationProviderService` class now has the methods necessary to start sending emails. +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. -Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service. +So, create the file `src/modules/algolia/index.ts` with the following content: -Create the file `src/modules/resend/index.ts` with the following content: +```ts title="src/modules/algolia/index.ts" +import { Module } from "@medusajs/framework/utils" +import AlgoliaModuleService from "./service" -```ts title="src/modules/resend/index.ts" -import { - ModuleProvider, - Modules, -} from "@medusajs/framework/utils" -import ResendNotificationProviderService from "./service" +export const ALGOLIA_MODULE = "algolia" -export default ModuleProvider(Modules.NOTIFICATION, { - services: [ResendNotificationProviderService], +export default Module(ALGOLIA_MODULE, { + service: AlgoliaModuleService, }) ``` -You export the module provider's definition using `ModuleProvider` from the Modules SDK. It accepts as a first parameter the name of the module that this provider belongs to, which is the Notification Module. It also accepts as a second parameter an object having a `service` property indicating the provider's service. +You use the `Module` function from the Modules SDK to create the module's definition. It accepts two parameters: -### Add Module to Configurations +1. The module's name, which is `algolia`. +2. An object with a required property `service` indicating the module's service. -Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations. +You also export the module's name as `ALGOLIA_MODULE` so you can reference it later. -Medusa's configurations are set in the `medusa-config.ts` file, which is at the root directory of your Medusa application. The configuration object accepts a `modules` array, whose value is an array of modules to add to the application. +### Add Module to Medusa's Configurations -Add the `modules` property to the exported configurations in `medusa-config.ts`: +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property and pass an array with your custom module: ```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ { - resolve: "@medusajs/medusa/notification", + resolve: "./src/modules/algolia", options: { - providers: [ - { - resolve: "./src/modules/resend", - id: "resend", - options: { - channels: ["email"], - api_key: process.env.RESEND_API_KEY, - from: process.env.RESEND_FROM_EMAIL, - }, - }, - ], + appId: process.env.ALGOLIA_APP_ID!, + apiKey: process.env.ALGOLIA_API_KEY!, + productIndexName: process.env.ALGOLIA_PRODUCT_INDEX_NAME!, }, }, ], }) ``` -In the `modules` array, you pass a module object having the following properties: +Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` package’s name. -- `resolve`: The NPM package of the Notification Module. Since the Resend Module is a Notification Module Provider, it'll be passed in the options of the Notification Module. -- `options`: An object of options to pass to the module. It has a `providers` property which is an array of module providers to register. Each module provider object has the following properties: - - `resolve`: The path to the module provider to register in the application. It can also be the name of an NPM package. - - `id`: A unique ID, which Medusa will use along with the `identifier` static property that you set earlier in the class to identify this module provider. - - `options`: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service. You must also specify the `channels` option, which indicates the channels that this provider sends notifications through. +You also pass an `options` property with the module's options, including the Algolia App ID, API Key, and the product index name. -Some of the module's options, such as the Resend API key, are set in environment variables. So, add the following environment variables to `.env`: +### Add Environment Variables -```shell -RESEND_FROM_EMAIL=onboarding@resend.dev -RESEND_API_KEY= +Before you can start using the Algolia Module, you need to set the environment variables for the Algolia App ID, API Key, and the product index name. + +Add the following environment variables to your `.env` file: + +```env +ALGOLIA_APP_ID=your-algolia-app-id +ALGOLIA_API_KEY=your-algolia-api-key +ALGOLIA_PRODUCT_INDEX_NAME=your-product-index-name ``` Where: -- `RESEND_FROM_EMAIL`: The email to send emails from. If you've configured the custom domain as explained in [Step 2](#step-2-prepare-resend-account), change this email to an email from your custom domain. Otherwise, you can use `onboarding@resend.dev` for development purposes. -- `RESEND_API_KEY` is the API key of your Resend account. To retrieve it: - - Go to API Keys in the sidebar. - - Click on the Create API Key button. +- `your-algolia-app-id` is your Algolia App ID. You can retrieve it from the Algolia dashboard by clicking at the application ID at the top left next to the sidebar. The pop up will show the application ID below the application's name. -![Click on the API keys in the sidebar, then click on the Create API Key button at the top right](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535399/Medusa%20Resources/Screenshot_2024-11-25_at_10.22.25_AM_v4d09s.png) +![Find the Algolia App ID by clicking on the application name in the Algolia dashboard, then copying the ID below the name in the pop-up](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815360/Medusa%20Resources/Screenshot_2025-03-24_at_1.19.30_PM_kdp3y5.png) -- In the form that opens, enter a name for the API key (for example, Medusa). You can keep its permissions to Full Access or change it to Sending Access. Once you're done, click Add. +- `your-algolia-api-key` is your Algolia API Key. To retrieve it from the Algolia dashboard: + 1. Click on Settings in the sidebar. + 2. Choose API Keys under "Team and Access". -![The form to create an API key with fields for the API key's name, permissions, and domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535464/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.26_AM_g7gcuc.png) +![In the settings page, find the Team and Access section at the right of the page and choose API Keys](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815534/Medusa%20Resources/Screenshot_2025-03-24_at_1.25.09_PM_hwsiba.png) -- A new pop-up will show with your API key hidden. Copy it before closing the pop-up, since you can't access the key again afterwards. Use its value for the `RESEND_API_KEY` environment variable. +3. Copy the Admin API Key. -![Click the copy icon to copy the API key](https://res.cloudinary.com/dza7lstvk/image/upload/v1732535791/Medusa%20Resources/Screenshot_2024-11-25_at_10.23.43_AM_divins.png) +- `your-product-index-name` is the name of the index where you'll store products. You can find it by going to Search -> Index, and copying the index name at the top of the page. -Your Resend Module Provider is all set up. You'll test it out in a later section. +![In the Algolia dashboard, go to Search -> Index and copy the index name at the top of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1742815790/Medusa%20Resources/Screenshot_2025-03-24_at_1.28.58_PM_yq10sf.png) + +Your module is now ready for use. You'll see how to use it in the next steps. *** -## Step 5: Add Order Confirmation Template +## Step 3: Sync Products to Algolia Workflow -In this step, you'll add a React template for order confirmation emails. You'll create it using the [react-email](https://github.com/resend/react-email) package you installed earlier. You can follow the same steps for other email templates, such as for customer confirmation. +To keep the Algolia index in sync with Medusa, you need to trigger indexing when products are created, updated, or deleted in Medusa. You can also allow the admin to manually trigger a reindex. -Create the directory `src/modules/resend/emails` that will hold the email templates. Then, to add the template for order confirmation, create the file `src/modules/resend/emails/order-placed.tsx` with the following content: +To implement the indexing functionality, you need to create a [workflow](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). A workflow is a series of 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. -```tsx title="src/modules/resend/emails/order-placed.tsx" highlights={templateHighlights} collapsibleLines="1-17" expandMoreLabel="Show Imports" -import { - Text, - Column, - Container, - Heading, - Html, - Img, - Row, - Section, - Tailwind, - Head, - Preview, - Body, - Link, -} from "@react-email/components" -import { BigNumberValue, CustomerDTO, OrderDTO } from "@medusajs/framework/types" +Learn more about workflows in the [Workflows documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md). -type OrderPlacedEmailProps = { - order: OrderDTO & { - customer: CustomerDTO - } - email_banner?: { - body: string - title: string - url: string - } -} +In this step, you'll create a workflow that indexes products in Algolia. In the next steps, you'll learn how to use the workflow when products are created, updated, or deleted, or when the admin manually triggers a reindex. -function OrderPlacedEmailComponent({ order, email_banner }: OrderPlacedEmailProps) { - const shouldDisplayBanner = email_banner && "title" in email_banner +The workflow has the following steps: - const formatter = new Intl.NumberFormat([], { - style: "currency", - currencyDisplay: "narrowSymbol", - currency: order.currency_code, - }) +- [useQueryGraphStep](https://docs.medusajs.com/references/helper-steps/useQueryGraphStep/index.html.md): Retrieve products matching specified filters and pagination parameters. +- [syncProductsStep](#syncProductsStep): Index products in Algolia. - const formatPrice = (price: BigNumberValue) => { - if (typeof price === "number") { - return formatter.format(price) - } +Medusa provides the `useQueryGraphStep` in its `@medusajs/medusa/core-flows` package. So, you only need to implement the second step. - if (typeof price === "string") { - return formatter.format(parseFloat(price)) - } +### syncProductsStep - return price?.toString() || "" - } +In the second step of the workflow, you create or update indexes in Algolia for the products retrieved in the first step. - return ( - - - - Thank you for your order from Medusa - - {/* Header */} -
- -
+To create the step, create the file `src/workflows/steps/sync-products.ts` with the following content: - {/* Thank You Message */} - - - Thank you for your order, {order.customer?.first_name || order.shipping_address?.first_name} - - - We're processing your order and will notify you when it ships. - - - - {/* Promotional Banner */} - {shouldDisplayBanner && ( - -
- - - - {email_banner.title} - - {email_banner.body} - - - - Shop Now - - - -
-
- )} - - {/* Order Items */} - - - Your Items - - - - Order ID: #{order.display_id} - - - {order.items?.map((item) => ( -
- - - {item.product_title - - - - {item.product_title} - - {item.variant_title} - - {formatPrice(item.total)} - - - -
- ))} - - {/* Order Summary */} -
- - Order Summary - - - - Subtotal - - - - {formatPrice(order.item_total)} - - - - {order.shipping_methods?.map((method) => ( - - - {method.name} - - - {formatPrice(method.total)} - - - ))} - - - Tax - - - {formatPrice(order.tax_total || 0)} - - - - - Total - - - {formatPrice(order.total)} - - -
-
- - {/* Footer */} -
- - If you have any questions, reply to this email or contact our support team at support@medusajs.com. - - - Order Token: {order.id} - - - © {new Date().getFullYear()} Medusajs, Inc. All rights reserved. - -
- - -
- ) -} - -export const orderPlacedEmail = (props: OrderPlacedEmailProps) => ( - -) -``` - -You define the `OrderPlacedEmailComponent` which is a React email template that shows the order's details, such as items and their totals. The component accepts an `order` object as a prop. - -You also export an `orderPlacedEmail` function, which accepts props as an input and returns the `OrderPlacedEmailComponent` passing it the props. Because you can't use JSX syntax in `src/modules/resend/service.ts`, you'll import this function instead. - -Next, update the `templates` variable in `src/modules/resend/service.ts` to assign this template to the `order-placed` template type: - -```ts title="src/modules/resend/service.ts" -// other imports... -import { orderPlacedEmail } from "./emails/order-placed" - -const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = { - [Templates.ORDER_PLACED]: orderPlacedEmail, -} -``` - -The `ResendNotificationProviderService` will now use the `OrderPlacedEmailComponent` as the template of order confirmation emails. - -### Test Email Out - -You'll later test out sending the email when an order is placed. However, you can also test out how the email looks like using [React Email's CLI tool](https://react.email/docs/cli). - -First, install the CLI tool in your Medusa application: - -```bash npm2yarn -npm install -D react-email -``` - -Then, in `src/modules/resend/emails/order-placed.tsx`, add the following at the end of the file: - -```tsx title="src/modules/resend/emails/order-placed.tsx" -const mockOrder = { - "order": { - "id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", - "display_id": 1, - "email": "afsaf@gmail.com", - "currency_code": "eur", - "total": 20, - "subtotal": 20, - "discount_total": 0, - "shipping_total": 10, - "tax_total": 0, - "item_subtotal": 10, - "item_total": 10, - "item_tax_total": 0, - "customer_id": "cus_01JSNXD6VQC1YH56E4TGC81NWX", - "items": [ - { - "id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9", - "title": "L", - "subtitle": "Medusa Sweatshirt", - "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", - "variant_id": "variant_01JSNXAQCZ5X81A3NRSVFJ3ZHQ", - "product_id": "prod_01JSNXAQBQ6MFV5VHKN420NXQW", - "product_title": "Medusa Sweatshirt", - "product_description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", - "product_subtitle": null, - "product_type": null, - "product_type_id": null, - "product_collection": null, - "product_handle": "sweatshirt", - "variant_sku": "SWEATSHIRT-L", - "variant_barcode": null, - "variant_title": "L", - "variant_option_values": null, - "requires_shipping": true, - "is_giftcard": false, - "is_discountable": true, - "is_tax_inclusive": false, - "is_custom_price": false, - "metadata": {}, - "raw_compare_at_unit_price": null, - "raw_unit_price": { - "value": "10", - "precision": 20, - }, - "created_at": new Date(), - "updated_at": new Date(), - "deleted_at": null, - "tax_lines": [], - "adjustments": [], - "compare_at_unit_price": null, - "unit_price": 10, - "quantity": 1, - "raw_quantity": { - "value": "1", - "precision": 20, - }, - "detail": { - "id": "orditem_01JSNXDH9DK1XMESEZPADYFWKY", - "version": 1, - "metadata": null, - "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", - "raw_unit_price": null, - "raw_compare_at_unit_price": null, - "raw_quantity": { - "value": "1", - "precision": 20, - }, - "raw_fulfilled_quantity": { - "value": "0", - "precision": 20, - }, - "raw_delivered_quantity": { - "value": "0", - "precision": 20, - }, - "raw_shipped_quantity": { - "value": "0", - "precision": 20, - }, - "raw_return_requested_quantity": { - "value": "0", - "precision": 20, - }, - "raw_return_received_quantity": { - "value": "0", - "precision": 20, - }, - "raw_return_dismissed_quantity": { - "value": "0", - "precision": 20, - }, - "raw_written_off_quantity": { - "value": "0", - "precision": 20, - }, - "created_at": new Date(), - "updated_at": new Date(), - "deleted_at": null, - "item_id": "ordli_01JSNXDH9C47KZ43WQ3TBFXZA9", - "unit_price": null, - "compare_at_unit_price": null, - "quantity": 1, - "fulfilled_quantity": 0, - "delivered_quantity": 0, - "shipped_quantity": 0, - "return_requested_quantity": 0, - "return_received_quantity": 0, - "return_dismissed_quantity": 0, - "written_off_quantity": 0, - }, - "subtotal": 10, - "total": 10, - "original_total": 10, - "discount_total": 0, - "discount_subtotal": 0, - "discount_tax_total": 0, - "tax_total": 0, - "original_tax_total": 0, - "refundable_total_per_unit": 10, - "refundable_total": 10, - "fulfilled_total": 0, - "shipped_total": 0, - "return_requested_total": 0, - "return_received_total": 0, - "return_dismissed_total": 0, - "write_off_total": 0, - "raw_subtotal": { - "value": "10", - "precision": 20, - }, - "raw_total": { - "value": "10", - "precision": 20, - }, - "raw_original_total": { - "value": "10", - "precision": 20, - }, - "raw_discount_total": { - "value": "0", - "precision": 20, - }, - "raw_discount_subtotal": { - "value": "0", - "precision": 20, - }, - "raw_discount_tax_total": { - "value": "0", - "precision": 20, - }, - "raw_tax_total": { - "value": "0", - "precision": 20, - }, - "raw_original_tax_total": { - "value": "0", - "precision": 20, - }, - "raw_refundable_total_per_unit": { - "value": "10", - "precision": 20, - }, - "raw_refundable_total": { - "value": "10", - "precision": 20, - }, - "raw_fulfilled_total": { - "value": "0", - "precision": 20, - }, - "raw_shipped_total": { - "value": "0", - "precision": 20, - }, - "raw_return_requested_total": { - "value": "0", - "precision": 20, - }, - "raw_return_received_total": { - "value": "0", - "precision": 20, - }, - "raw_return_dismissed_total": { - "value": "0", - "precision": 20, - }, - "raw_write_off_total": { - "value": "0", - "precision": 20, - }, - }, - ], - "shipping_address": { - "id": "caaddr_01JSNXD6W0TGPH2JQD18K97B25", - "customer_id": null, - "company": "", - "first_name": "safasf", - "last_name": "asfaf", - "address_1": "asfasf", - "address_2": "", - "city": "asfasf", - "country_code": "dk", - "province": "", - "postal_code": "asfasf", - "phone": "", - "metadata": null, - "created_at": "2025-04-25T07:25:48.801Z", - "updated_at": "2025-04-25T07:25:48.801Z", - "deleted_at": null, - }, - "billing_address": { - "id": "caaddr_01JSNXD6W0V7RNZH63CPG26K5W", - "customer_id": null, - "company": "", - "first_name": "safasf", - "last_name": "asfaf", - "address_1": "asfasf", - "address_2": "", - "city": "asfasf", - "country_code": "dk", - "province": "", - "postal_code": "asfasf", - "phone": "", - "metadata": null, - "created_at": "2025-04-25T07:25:48.801Z", - "updated_at": "2025-04-25T07:25:48.801Z", - "deleted_at": null, - }, - "shipping_methods": [ - { - "id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1", - "name": "Standard Shipping", - "description": null, - "is_tax_inclusive": false, - "is_custom_amount": false, - "shipping_option_id": "so_01JSNXAQA64APG6BNHGCMCTN6V", - "data": {}, - "metadata": null, - "raw_amount": { - "value": "10", - "precision": 20, - }, - "created_at": new Date(), - "updated_at": new Date(), - "deleted_at": null, - "tax_lines": [], - "adjustments": [], - "amount": 10, - "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", - "detail": { - "id": "ordspmv_01JSNXDH9B5RAF4FH3M1HH3TEA", - "version": 1, - "order_id": "order_01JSNXDH9BPJWWKVW03B9E9KW8", - "return_id": null, - "exchange_id": null, - "claim_id": null, - "created_at": new Date(), - "updated_at": new Date(), - "deleted_at": null, - "shipping_method_id": "ordsm_01JSNXDH9B9DDRQXJT5J5AE5V1", - }, - "subtotal": 10, - "total": 10, - "original_total": 10, - "discount_total": 0, - "discount_subtotal": 0, - "discount_tax_total": 0, - "tax_total": 0, - "original_tax_total": 0, - "raw_subtotal": { - "value": "10", - "precision": 20, - }, - "raw_total": { - "value": "10", - "precision": 20, - }, - "raw_original_total": { - "value": "10", - "precision": 20, - }, - "raw_discount_total": { - "value": "0", - "precision": 20, - }, - "raw_discount_subtotal": { - "value": "0", - "precision": 20, - }, - "raw_discount_tax_total": { - "value": "0", - "precision": 20, - }, - "raw_tax_total": { - "value": "0", - "precision": 20, - }, - "raw_original_tax_total": { - "value": "0", - "precision": 20, - }, - }, - ], - "customer": { - "id": "cus_01JSNXD6VQC1YH56E4TGC81NWX", - "company_name": null, - "first_name": null, - "last_name": null, - "email": "afsaf@gmail.com", - "phone": null, - "has_account": false, - "metadata": null, - "created_by": null, - "created_at": "2025-04-25T07:25:48.791Z", - "updated_at": "2025-04-25T07:25:48.791Z", - "deleted_at": null, - }, - }, -} -// @ts-ignore -export default () => -``` - -You create a mock order object that contains the order's details. Then, you export a default function that returns the `OrderPlacedEmailComponent` passing it the mock order. - -The React Email CLI tool will use the function to render the email template. - -Finally, add the following script to `package.json`: - -```json -{ - "scripts": { - "dev:email": "email dev --dir ./src/modules/resend/emails" - } -} -``` - -This script will run the React Email CLI tool, passing it the directory where the email templates are located. - -You can now test out the email template by running the following command: - -```bash npm2yarn -npm run dev:email -``` - -This will start a development server at `http://localhost:3000`. If you open this URL, you can view your email templates in the browser. - -You can make changes to the email template, and the server will automatically reload the changes. - -![The email template rendered in the browser](https://res.cloudinary.com/dza7lstvk/image/upload/v1745568201/Medusa%20Resources/Screenshot_2025-04-25_at_10.41.26_AM_u86abc.png) - -*** - -## Step 6: Send Email when Order is Placed - -Medusa has an event system that emits an event when a commerce operation is performed. You can then listen and handle that event in an asynchronous function called a subscriber. - -So, to send a confirmation email when a customer places an order, which is a commerce operation that Medusa already implements, you don't need to extend or hack your way into Medusa's implementation as you would do with other commerce platforms. - -Instead, you'll create a subscriber that listens to the `order.placed` event and sends an email when the event is emitted. - -Learn more about Medusa's event system in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). - -### Send Order Confirmation Email Workflow - -To send the order confirmation email, you need to retrieve the order's details first, then use the Notification Module's service to send the email. To implement this flow, you'll create 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 a subscriber. - -Learn more about workflows in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/index.html.md) - -#### Send Notification Step - -You'll start by implementing the step of the workflow that sends the notification. To do that, create the file `src/workflows/steps/send-notification.ts` with the following content: - -```ts title="src/workflows/steps/send-notification.ts" -import { Modules } from "@medusajs/framework/utils" +```ts title="src/workflows/steps/sync-products.ts" +import { ProductDTO } from "@medusajs/framework/types" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" -import { CreateNotificationDTO } from "@medusajs/framework/types" +import { ALGOLIA_MODULE } from "../../modules/algolia" +import AlgoliaModuleService from "../../modules/algolia/service" -export const sendNotificationStep = createStep( - "send-notification", - async (data: CreateNotificationDTO[], { container }) => { - const notificationModuleService = container.resolve( - Modules.NOTIFICATION +export type SyncProductsStepInput = { + products: ProductDTO[] +} + +export const syncProductsStep = createStep( + "sync-products", + async ({ products }: SyncProductsStepInput, { container }) => { + const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE) + + const existingProducts = (await algoliaModuleService.retrieveFromIndex( + products.map((product) => product.id), + "product" + )).results.filter(Boolean) + const newProducts = products.filter( + (product) => !existingProducts.some((p) => p.objectID === product.id) ) - const notification = await notificationModuleService.createNotifications(data) - return new StepResponse(notification) + await algoliaModuleService.indexData( + products as unknown as Record[], + "product" + ) + + return new StepResponse(undefined, { + newProducts: newProducts.map((product) => product.id), + existingProducts, + }) + } + // TODO add compensation +) +``` + +You create a step with `createStep` from the Workflows SDK. It accepts two parameters: + +1. The step's unique name, which is `sync-products`. +2. An async function that receives two parameters: + - The step's input, which is in this case an object holding an array of products to sync into Algolia. + - An object that has properties including the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools that you can access in the step. + +In the step function, you resolve the Algolia Module's service from the Medusa container using the name you exported in the module definition's file. + +Then, you retrieve the products that are already indexed in Algolia and determine which products are new. You'll learn why this is useful in a bit. + +Finally, you pass the products you received in the input to Algolia to create or update its indices. + +A step function must return a `StepResponse` instance. The `StepResponse` constructor accepts two parameters: + +1. The step's output, which in this case is `undefined`. +2. Data to pass to the step's compensation function. + +#### Compensation Function + +The compensation function undoes the actions performed in a step. Then, if an error occurs during the workflow's execution, the compensation functions of executed steps are called to roll back the changes. This mechanism ensures data consistency in your application, especially as you integrate external systems. + +To add a compensation function to a step, pass it as a third parameter to `createStep`: + +```ts title="src/workflows/steps/sync-products.ts" +export const syncProductsStep = createStep( + // ... + async (input, { container }) => { + if (!input) { + return + } + + const algoliaModuleService: AlgoliaModuleService = container.resolve(ALGOLIA_MODULE) + + if (input.newProducts) { + await algoliaModuleService.deleteFromIndex( + input.newProducts, + "product" + ) + } + + if (input.existingProducts) { + await algoliaModuleService.indexData( + input.existingProducts, + "product" + ) + } } ) ``` -You define the `sendNotificationStep` using the `createStep` function that accepts two parameters: +The compensation function receives two parameters: -- A string indicating the step's unique name. -- The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second. +1. The data you passed as a second parameter of `StepResponse` in the step function. +2. A context object similar to the step function that holds the Medusa container. -The `container` property in the second parameter is an instance of the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md), which is a registry of Framework and commerce tools, such as a module's service, that you can resolve to utilize their functionalities. +In the compensation function, you resolve the Algolia Module's service from the container. Then, you delete from Algolia the products that were newly indexed, and revert the existing products to their original data. -The Medusa container is accessible by all customizations, such as workflows and subscribers, except for modules. Each module has its own container with Framework tools like the Logger utility. +### Add Sync Products Workflow -In the step function, you resolve the Notification Module's service, and use its `createNotifications` method, passing it the notification's data that the step receives as an input. +You can now create the worklow that syncs the products to Algolia. -The step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a parameter the data to return to the workflow that executed this step. +To create the workflow, create the file `src/workflows/sync-products.ts` with the following content: -#### Workflow Implementation - -You'll now create the workflow that uses the `sendNotificationStep` to send the order confirmation email. - -Create the file `src/workflows/send-order-confirmation.ts` with the following content: - -```ts title="src/workflows/send-order-confirmation.ts" highlights={workflowHighlights} -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" +```ts title="src/workflows/sync-products.ts" +import { createWorkflow, WorkflowResponse } from "@medusajs/framework/workflows-sdk" import { useQueryGraphStep } from "@medusajs/medusa/core-flows" -import { sendNotificationStep } from "./steps/send-notification" +import { syncProductsStep, SyncProductsStepInput } from "./steps/sync-products" -type WorkflowInput = { - id: string +type SyncProductsWorkflowInput = { + filters?: Record + limit?: number + offset?: number } -export const sendOrderConfirmationWorkflow = createWorkflow( - "send-order-confirmation", - ({ id }: WorkflowInput) => { +export const syncProductsWorkflow = createWorkflow( + "sync-products", + ({ filters, limit, offset }: SyncProductsWorkflowInput) => { // @ts-ignore - const { data: orders } = useQueryGraphStep({ - entity: "order", - fields: [ - "id", - "display_id", - "email", - "currency_code", - "total", - "items.*", - "shipping_address.*", - "billing_address.*", - "shipping_methods.*", - "customer.*", - "total", - "subtotal", - "discount_total", - "shipping_total", - "tax_total", - "item_subtotal", - "item_total", - "item_tax_total", - ], + const { data, metadata } = useQueryGraphStep({ + entity: "product", + fields: ["id", "title", "description", "handle", "thumbnail", "categories.*", "tags.*"], + pagination: { + take: limit, + skip: offset, + }, filters: { - id, + // @ts-ignore + status: "published", + ...filters, }, }) - - const notification = sendNotificationStep([{ - to: orders[0].email, - channel: "email", - template: "order-placed", - data: { - order: orders[0], - }, - }]) - return new WorkflowResponse(notification) + syncProductsStep({ + products: data, + } as SyncProductsStepInput) + + return new WorkflowResponse({ + products: data, + metadata, + }) } ) ``` You create a workflow using `createWorkflow` from the Workflows SDK. It accepts the workflow's unique name as a first parameter. -It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps: +It accepts as a second parameter a constructor function, which is the workflow's implementation. The function can accept input, which in this case is pagination and filter parameters for the products to retrieve. -1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details. -2. `sendNotificationStep` which is the step you implemented. You pass it an array with one object, which is the notification's details having following properties: - - `to`: The address to send the email to. You pass the customer's email that is stored in the order. - - `channel`: The channel to send the notification through, which is `email`. Since you specified `email` in the Resend Module Provider's `channel` option, the Notification Module will delegate the sending to the Resend Module Provider's service. - - `template`: The email's template type. You retrieve the template content in the `ResendNotificationProviderService`'s `send` method based on the template specified here. - - `data`: The data to pass to the email template, which is the order's details. +In the workflow's constructor function, you: -A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/workflows/constructor-constraints/index.html.md). +1. Execute `useQueryGraphStep` to retrieve products from Medusa's database. This step uses Medusa's [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md) tool to retrieve data across modules. You pass it the pagination and filter parameters you received in the input. +2. Execute `syncProductsStep` to index the products in Algolia. You pass it the products you retrieved in the previous step. -You'll execute the workflow when you create the subscriber next. +A workflow must return an instance of `WorkflowResponse`. The `WorkflowResponse` constructor accepts the workflow's output as a parameter, which is an object holding the retrieved products and their pagination details. -#### Add the Order Placed Subscriber +In the next step, you'll learn how to execute this workflow. -Now that you have the workflow to send an order-confirmation email, you'll execute it in a subscriber that's executed whenever an order is placed. +*** -You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/order-placed.ts` with the following content: +## Step 4: Trigger Algolia Sync Manually -```ts title="src/subscribers/order-placed.ts" highlights={subscriberHighlights} -import type { +As mentioned earlier, you'll trigger the Algolia sync automatically when product events occur, but you also want to allow the admin to manually trigger a reindex. + +In this step, you'll add the functionality to trigger the `syncProductsWorkflow` manually from the Medusa Admin dashboard. This requires: + +1. Creating a subscriber that listens to a custom `algolia.sync` event to trigger syncing products to Algolia. +2. Creating an API route that the Medusa Admin dashboard can call to emit the `algolia.sync` event, which triggers the subscriber. +3. Add a new page or UI route to the Medusa Admin dashboard to allow the admin to trigger the reindex. + +### Create Products Sync Subscriber + +A subscriber is an asynchronous function that listens to one or more events and performs actions when these events are emitted. A subscriber is useful when syncing data across systems, as the operation can be time-consuming and should be performed in the background. + +Learn more about subscribers in the [Events and Subscribers documentation](https://docs.medusajs.com/docs/learn/fundamentals/events-and-subscribers/index.html.md). + +You create a subscriber in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create the subscriber that listens to the `algolia.sync` event, create the file `src/subscribers/algolia-sync.ts` with the following content: + +```ts title="src/subscribers/algolia-sync.ts" +import { SubscriberArgs, - SubscriberConfig, + type SubscriberConfig, } from "@medusajs/framework" -import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation" +import { syncProductsWorkflow } from "../workflows/sync-products" -export default async function orderPlacedHandler({ +export default async function algoliaSyncHandler({ + container, +}: SubscriberArgs) { + const logger = container.resolve("logger") + + let hasMore = true + let offset = 0 + const limit = 50 + let totalIndexed = 0 + + logger.info("Starting product indexing...") + + while (hasMore) { + const { result: { products, metadata } } = await syncProductsWorkflow(container) + .run({ + input: { + limit, + offset, + }, + }) + + hasMore = offset + limit < (metadata?.count ?? 0) + offset += limit + totalIndexed += products.length + } + + logger.info(`Successfully indexed ${totalIndexed} products`) +} + +export const config: SubscriberConfig = { + event: "algolia.sync", +} +``` + +A subscriber file must export: + +1. An asynchronous function, which is the subscriber that is executed when the event is emitted. +2. A configuration object that holds the name of the event the subscriber listens to, which is `algolia.sync` in this case. + +The subscriber function receives an object as a parameter that has a `container` property, which is the Medusa container. + +In the subscriber function, you initialize variables to keep track of the pagination and the total number of products indexed. + +Then, you start a loop that retrieves products in batches of 50 and indexes them in Algolia using the `syncProductsWorkflow`. Finally, you log the total number of products indexed. + +You'll learn how to emit the `algolia.sync` event next. + +If you want to sync other data types, you can do it in this subscriber as well. + +### Create API Route to Trigger Sync + +To allow the Medusa Admin dashboard to trigger the `algolia.sync` event, you need to create an API route that emits the event. + +An API Route is an endpoint that exposes commerce features to external applications and clients, such as storefronts. + +Learn more about API routes in [this documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/index.html.md). + +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`. + +So, to create an API route at the path `/admin/algolia/sync`, create the file `src/api/admin/algolia/sync/route.ts` with the following content: + +```ts title="src/api/admin/algolia/sync/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { Modules } from "@medusajs/framework/utils" + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const eventModuleService = req.scope.resolve(Modules.EVENT_BUS) + await eventModuleService.emit({ + name: "algolia.sync", + data: {}, + }) + res.send({ + message: "Syncing data to Algolia", + }) +} +``` + +Since you export a `POST` route handler function, you expose an `API` route at `/admin/algolia/sync`. The route handler function accepts two parameters: + +1. A request object with details and context on the request, such as body parameters or authenticated user details. +2. A response object to manipulate and send the response. + +In the route handler, you use the Medusa container that is available in the request object to resolve the [Event Module](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/infrastructure-modules/event/index.html.md). This module manages events and their subscribers. + +Then, you emit the `algolia.sync` event using the Event Module's `emit` method, passing it the event name. + +Finally, you send a response with a message indicating that data is being synced to Algolia. + +### Add Algolia Sync Page to Admin Dashboard + +The last step is to add a new page to the admin dashboard that allows the admin to trigger the reindex. You add a new page using a [UI Route](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). + +A UI route is a React component that specifies the content to be shown in a new page in the Medusa Admin dashboard. You'll create a UI route to display a button that triggers the reindex when clicked. + +Learn more about UI routes in the [UI Routes documentation](https://docs.medusajs.com/docs/learn/fundamentals/admin/ui-routes/index.html.md). + +#### Configure JS SDK + +Before creating the UI route, you'll configure Medusa's [JS SDK](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/js-sdk/index.html.md) that you can use to send requests to the Medusa server from any client application, including your Medusa Admin customizations. + +The JS SDK is installed by default in your Medusa application. To configure it, create the file `src/admin/lib/sdk.ts` with the following content: + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: "http://localhost:9000", + debug: process.env.NODE_ENV === "development", + auth: { + type: "session", + }, +}) +``` + +You create an instance of the JS SDK using the `Medusa` class from the JS SDK. You pass it an object having the following properties: + +- `baseUrl`: The base URL of the Medusa server. +- `debug`: A boolean indicating whether to log debug information into the console. +- `auth`: An object specifying the authentication type. When using the JS SDK for admin customizations, you use the `session` authentication type. + +#### Create UI Route + +You'll now create the UI route that displays a button to trigger the reindex. You create a UI route in a `page.tsx` file under a sub-directory of `src/admin/routes` directory. The file's path relative to `src/admin/routes` determines its path in the dashboard. + +So, to create a new page under the Settings section of the Medusa Admin, create the file `src/admin/routes/settings/algolia/page.tsx` with the following content: + +```tsx title="src/admin/routes/settings/algolia/page.tsx" +import { Container, Heading, Button, toast } from "@medusajs/ui" +import { useMutation } from "@tanstack/react-query" +import { sdk } from "../../../lib/sdk" +import { defineRouteConfig } from "@medusajs/admin-sdk" + +const AlgoliaPage = () => { + const { mutate, isPending } = useMutation({ + mutationFn: () => + sdk.client.fetch("/admin/algolia/sync", { + method: "POST", + }), + onSuccess: () => { + toast.success("Successfully triggered data sync to Algolia") + }, + onError: (err) => { + console.error(err) + toast.error("Failed to sync data to Algolia") + }, + }) + + const handleSync = () => { + mutate() + } + + return ( + +
+ Algolia Sync +
+
+ +
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Algolia", +}) + +export default AlgoliaPage +``` + +A UI route's file must export: + +1. A React component that defines the content of the page. +2. A configuration object that specifies the route's label in the dashboard. This label is used to show a sidebar item for the new route. + +In the React component, you use `useMutation` hook from `@tanstack/react-query` to create a mutation that sends a `POST` request to the API route you created earlier. In the mutation function, you use the JS SDK to send the request. + +Then, in the return statement, you display a button that triggers the mutation when clicked, which sends a request to the API route you created earlier. + +### Test it Out + +You'll now test out the entire flow, starting from triggering the reindex manually from the Medusa Admin dashboard, to checking the Algolia dashboard for the indexed products. + +Run the following command to start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, open the Medusa Admin at `http://localhost:9000/app` and log in with the credentials you set up in the first step. + +Can't remember the credentials? Learn how to create a user in the [Medusa CLI reference](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/medusa-cli/commands/user/index.html.md). + +After you log in, go to Settings from the sidebar. You'll find in the Settings' sidebar a new "Algolia" item. If you click on it, you'll find the page you created with the button to sync products to Algolia. + +If you click on the button, the products will be synced to Algolia. + +![The Algolia Sync page in the Medusa Admin dashboard with a button to sync products to Algolia](https://res.cloudinary.com/dza7lstvk/image/upload/v1742820813/Medusa%20Resources/Screenshot_2025-03-24_at_2.52.31_PM_eiegzb.png) + +You can check that the sync ran and was completed by checking the Medusa logs in the terminal where you started the Medusa application. You should find the following messages: + +```bash +info: Processing algolia.sync which has 1 subscribers +info: Starting product indexing... +info: Successfully indexed 4 products +``` + +These messages indicate that the `algolia.sync` event was emitted, which triggered the subscriber you created to sync the products using the `syncProductsWorkflow`. + +Finally, you can check the Algolia dashboard to see the indexed products. Go to Search -> Index, and check the records of the index you set up in the Algolia Module's options (`products`, for example). + +![The Algolia dashboard showing the indexed products](https://res.cloudinary.com/dza7lstvk/image/upload/v1742821034/Medusa%20Resources/Screenshot_2025-03-24_at_2.56.38_PM_mtojrv.png) + +*** + +## Step 5: Update Index on Product Changes + +You'll now automate the indexing of the products whenever a change occurs. That includes when a product is created, updated, or deleted. + +Similar to before, you'll create subscribers to listen to these events. + +### Handle Create and Update Products + +The action to perform when a product is created or updated is the same. You'll use the `syncProductsWorkflow` to sync the product to Algolia. + +So, you only need one subscriber to handle these two events. To create the subscriber, create the file `src/subscribers/product-sync.ts` with the following content: + +```ts title="src/subscribers/product-sync.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { syncProductsWorkflow } from "../workflows/sync-products" + +export default async function handleProductEvents({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - await sendOrderConfirmationWorkflow(container) + await syncProductsWorkflow(container) .run({ input: { - id: data.id, + filters: { + id: data.id, + }, }, }) } export const config: SubscriberConfig = { - event: "order.placed", + event: ["product.created", "product.updated"], } ``` -A subscriber file exports: +The subscriber listens to the `product.created` and `product.updated` events. When either of these events is emitted, the subscriber triggers the `syncProductsWorkflow` to sync the product to Algolia. -- An asynchronous function that's executed whenever the associated event is emitted, which is the `order.placed` event. -- A configuration object with an `event` property indicating the event the subscriber is listening to. +When the `product.created` and `product.updated` events are emitted, the product's ID is passed in the event data payload, which you can access in the `event.data` property of the subscriber function's parameter. -The subscriber function accepts the event's details as a first paramter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](https://docs.medusajs.com/docs/learn/fundamentals/medusa-container/index.html.md). +So, you pass the product's ID to the `syncProductsWorkflow` as a filter to retrieve only the product that was created or updated. -In the function, you execute the `sendOrderConfirmationWorkflow` by invoking it, passing it the `container`, then using its `run` method. The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload. +#### Test it Out -This subscriber now runs whenever an order is placed. You'll see this in action in the next section. +To test it out, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, either create a product or update an existing one using the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was created or updated. + +### Handle Product Deletion + +When a product is deleted, you need to remove it from the Algolia index. As this requires a different action than creating or updating a product, you'll create a new workflow that deletes the product from Algolia, then create a subscriber that listens to the `product.deleted` event to trigger the workflow. + +#### Create Delete Product Step + +The workflow to delete a product from Algolia will have only one step that deletes products by their IDs from Algolia. + +So, create the step at `src/workflows/steps/delete-products-from-algolia.ts` with the following content: + +```ts title="src/workflows/steps/delete-products-from-algolia.ts" +import { + createStep, + StepResponse, +} from "@medusajs/framework/workflows-sdk" +import { ALGOLIA_MODULE } from "../../modules/algolia" + +export type DeleteProductsFromAlgoliaWorkflow = { + ids: string[] +} + +export const deleteProductsFromAlgoliaStep = createStep( + "delete-products-from-algolia-step", + async ( + { ids }: DeleteProductsFromAlgoliaWorkflow, + { container } + ) => { + const algoliaModuleService = container.resolve(ALGOLIA_MODULE) + + const existingRecords = await algoliaModuleService.retrieveFromIndex( + ids, + "product" + ) + await algoliaModuleService.deleteFromIndex( + ids, + "product" + ) + + return new StepResponse(undefined, existingRecords) + }, + async (existingRecords, { container }) => { + if (!existingRecords) { + return + } + const algoliaModuleService = container.resolve(ALGOLIA_MODULE) + + await algoliaModuleService.indexData( + existingRecords as unknown as Record[], + "product" + ) + } +) +``` + +The step receives the IDs of the products to delete as an input. + +In the step, you resolve the Algolia Module's service and retrieve the existing records from Algolia. This is useful to revert the deletion if an error occurs. + +Then, you delete the products from Algolia and pass the existing records to the compensation function. + +In the compensation function, you reindex the existing records if an error occurs. + +#### Create Delete Product Workflow + +You can now create the workflow that deletes products from Algolia. Create the file `src/workflows/delete-products-from-algolia.ts` with the following content: + +```ts title="src/workflows/delete-products-from-algolia.ts" +import { createWorkflow } from "@medusajs/framework/workflows-sdk" +import { deleteProductsFromAlgoliaStep } from "./steps/delete-products-from-algolia" + +type DeleteProductsFromAlgoliaWorkflowInput = { + ids: string[] +} + +export const deleteProductsFromAlgoliaWorkflow = createWorkflow( + "delete-products-from-algolia", + (input: DeleteProductsFromAlgoliaWorkflowInput) => { + deleteProductsFromAlgoliaStep(input) + } +) +``` + +The workflow receives an object with the IDs of the products to delete. It then executes the `deleteProductsFromAlgoliaStep` to delete the products from Algolia. + +#### Create Delete Product Subscriber + +Finally, you'll create the subscriber that listens to the `product.deleted` event to trigger the above workflow. + +Create the file `src/subscribers/product-delete.ts` with the following content: + +```ts title="src/subscribers/product-delete.ts" +import { SubscriberArgs, type SubscriberConfig } from "@medusajs/framework" +import { deleteProductsFromAlgoliaWorkflow } from "../workflows/delete-products-from-algolia" + +export default async function handleProductDeleted({ + event: { data }, + container, +}: SubscriberArgs<{ id: string }>) { + await deleteProductsFromAlgoliaWorkflow(container) + .run({ + input: { + ids: [data.id], + }, + }) +} + +export const config: SubscriberConfig = { + event: "product.deleted", +} +``` + +The subscriber listens to the `product.deleted` event. When the event is emitted, the subscriber triggers the `deleteProductsFromAlgoliaWorkflow`, passing it the ID of the product to delete. + +#### Test it Out + +To test product deletion, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, delete a product from the Medusa Admin dashboard. If you check the Algolia dashboard, you'll find that the product was deleted there as well. *** -## Test it Out: Place an Order +## Step 6: Add Search API Route -To test out the Resend integration, you'll place an order using the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) that you installed as part of installing Medusa. +Before customizing the storefront to show the search UI, you'll create an API route in your Medusa application that allows storefronts to search products in Algolia. -Start your Medusa application first: +While you can implement the search functionality directly in the storefront to interact with Algolia, this approach centralizes your search integration in Medusa, allowing you to change or modify the integration as necessary. You can also rely on the same behavior and results across different storefronts. -```bash npm2yarn -npm run dev +To implement the API Route, create the file `src/api/store/products/search/route.ts` with the following content: + +```ts title="src/api/store/products/search/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" +import { ALGOLIA_MODULE } from "../../../../modules/algolia" +import AlgoliaModuleService from "../../../../modules/algolia/service" +import { z } from "zod" + +export const SearchSchema = z.object({ + query: z.string(), +}) + +type SearchRequest = z.infer + +export async function POST( + req: MedusaRequest, + res: MedusaResponse +) { + const algoliaModuleService: AlgoliaModuleService = req.scope.resolve(ALGOLIA_MODULE) + + const { query } = req.validatedBody + + const results = await algoliaModuleService.search( + query as string + ) + + res.json(results) +} ``` -Then, in the Next.js Starter Storefront's directory (which was installed in a directory outside of the Medusa application's directory with the name `{project-name}-storefront`, where `{project-name}` is the name of the Medusa application's directory), run the following command to start the storefront: +You first define a schema with [Zod](https://zod.dev/), a library to define validation schemas. The schema defines the structure of the request body, which in this case is an object with a `query` property of type `string`. Later, you'll use the schema to enforce request body validation. -```bash npm2yarn -npm run dev +Then, you expose a `POST` API Route at `/store/search` that searches Algolia using the `search` method you implemented in the Algolia Module's service. You pass to it the query string from the request body. + +Finally, you return the search results as it is in the response. + +### Add Validation Middleware + +To ensure that requests sent to the API route have the required request body parameters, you can use 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. It's executed before the route handler. + +Learn more about middleware in the [Middlewares documentation](https://docs.medusajs.com/docs/learn/fundamentals/api-routes/middlewares/index.html.md). + +Middlewares are created in the `src/api/middlewares.ts` file. So create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { SearchSchema } from "./store/products/search/route" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/products/search", + method: ["POST"], + middlewares: [ + validateAndTransformBody(SearchSchema), + ], + }, + ], +}) ``` -Then, open the storefront in your browser at `http://localhost:8000` and: +To export the middlewares, you use the `defineMiddlewares` function. It accepts an object having a `routes` property, whose value is an array of middleware route objects. Each middleware route object has the following properties: -1. Go to Menu -> Store. +- `matcher`: The path of the route the middleware applies to. +- `method`: The HTTP methods the middleware applies to, which is in this case `POST`. +- `middlewares`: An array of middleware functions to apply to the route. You apply the `validateAndTransformBody` middleware which ensures that a request's body has the required parameters. You pass it the schema you defined earlier in the API route's file. -![Choose Store from Menu](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539139/Medusa%20Resources/Screenshot_2024-11-25_at_2.51.59_PM_fubiwj.png) +You can use the search API route now. You'll see it in action as you customize the storefront in the next step. -2\. Click on a product, select its options, and add it to the cart. +*** -![Choose an option, such as size, then click on the Add to cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539227/Medusa%20Resources/Screenshot_2024-11-25_at_2.53.11_PM_iswcjy.png) +## Step 7: Search Products in Next.js Starter Storefront -3\. Click on Cart at the top right, then click Go to Cart. +The last step is to provide the search functionalities to customers on your storefront. In the first step, you installed the [Next.js Starter Storefront](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/nextjs-starter/index.html.md) along with the Medusa application. -![Cart is at the top right. It opens a dropdown with a Go to Cart button](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539354/Medusa%20Resources/Screenshot_2024-11-25_at_2.54.44_PM_b1pnlu.png) +In this step, you'll customize the Next.js Starter Storefront to add the search functionality. -4\. On the cart's page, click on the "Go to checkout" button. +The Next.js Starter Storefront was installed in a separate directory from Medusa. The directory's name is `{your-project}-storefront`. -![The Go to checkout button is at the right side of the page](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539443/Medusa%20Resources/Screenshot_2024-11-25_at_2.56.27_PM_cvqshj.png) - -5\. On the checkout page, when entering the shipping address, make sure to set the email to your Resend account's email if you didn't set up a custom domain. - -![Enter your Resend account email if you didn't set up a custom domain](https://res.cloudinary.com/dza7lstvk/image/upload/v1732539536/Medusa%20Resources/Screenshot_2024-11-25_at_2.58.31_PM_wmlh60.png) - -6\. After entering the shipping address, choose a delivery and payment methods, then click the Place Order button. - -Once the order is placed, you'll find the following message logged in the Medusa application's terminal: +So, if your Medusa application's directory is `medusa-search`, you can find the storefront by going back to the parent directory and changing to the `medusa-search-storefront` directory: ```bash -info: Processing order.placed which has 1 subscribers +cd ../medusa-search-storefront # change based on your project name ``` -This indicates that the `order.placed` event was emitted and its subscriber, which you added in the previous step, is executed. +### Install Algolia Packages -If you check the inbox of the email address you specified in the shipping address, you'll find a new email with the order's details. +Before adding the implementation of the search functionality, you need to install the Algolia packages necessary to add the search functionality in your storefront. -![Example of order-confirmation email](https://res.cloudinary.com/dza7lstvk/image/upload/v1732551372/Medusa%20Resources/Screenshot_2024-11-25_at_6.15.59_PM_efyuoj.png) +Run the following command in the directory of your Next.js Starter Storefront: + +```bash npm2yarn +npm install algoliasearch react-instantsearch +``` + +This installs the Algolia Search JavaScript client and the React InstantSearch library, which you'll use to build the search functionality. + +### Add Search Client Configuration + +Next, you need to configure the search client. Not only do you need to initialize Algolia, but you also need to change the searching mechanism to use your custom API route in the Medusa application instead of Algolia's API directly. + +In `src/lib/config.ts`, add the following imports at the top of the file: + +```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue" +import { + liteClient as algoliasearch, + LiteClient as SearchClient, +} from "algoliasearch/lite" +``` + +Then, add the following at the end of the file: + +```ts title="src/lib/config.ts" badgeLabel="Storefront" badgeColor="blue" +export const searchClient: SearchClient = { + ...(algoliasearch( + process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "", + process.env.NEXT_PUBLIC_ALGOLIA_API_KEY || "" + )), + search: async (params) => { + const request = Array.isArray(params) ? params[0] : params + const query = "params" in request ? request.params?.query : + "query" in request ? request.query : "" + + if (!query) { + return { + results: [ + { + hits: [], + nbHits: 0, + nbPages: 0, + page: 0, + hitsPerPage: 0, + processingTimeMS: 0, + query: "", + params: "", + }, + ], + } + } + + return await sdk.client.fetch(`/store/products/search`, { + method: "POST", + body: { + query, + }, + }) + }, +} +``` + +In the code above, you create a `searchClient` object that initializes the Algolia client with your Algolia App ID and API Key. + +You also define a `search` method that sends a `POST` request to the search API route you created in the Medusa application. You use the JS SDK (which is initialized in this same file) to send the request. + +### Set Environment Variables + +In the storefront's `.env.local` file, add the following Algolia-related environment variables: + +```plain badgeLabel="Storefront" badgeColor="blue" +NEXT_PUBLIC_ALGOLIA_APP_ID=your_algolia_app_id +NEXT_PUBLIC_ALGOLIA_API_KEY=your_algolia_api_key +NEXT_PUBLIC_ALGOLIA_PRODUCT_INDEX_NAME=your-products-index-name +``` + +Where: + +- `your_algolia_app_id` is your Algolia App ID, as explained in the [Add Environment Variables section](#add-environment-variables) earlier. +- `your_algolia_api_key` is your Algolia Search API key. You can retrieve it from the [same API keys page on the Algolia dashboard](#add-environment-variables) that you retrieved the Admin API key from. +- `your-products-index-name` is the name of the index you created in Algolia to store the products, which you can retrieve as explained in the [Add Environment Variables section](#add-environment-variables) earlier. You'll use this variable later. + +Do not expose your Admin API key in the storefront. The Admin API key should only be used in the Medusa application to interact with Algolia, as it has full access to your Algolia account. + +### Add Search Modal Component + +You'll now add a search modal component that customers can use to search for products. The search modal will display the search results in real-time as the customer types in the search query. + +Later, you'll add the search modal to the navigation bar, allowing customers to open the search modal from any page. + +Create the file `src/modules/search/components/modal/index.tsx` with the following content: + +```tsx title="src/modules/search/components/modal/index.tsx" badgeLabel="Storefront" badgeColor="blue" +"use client" + +import React, { useEffect, useState } from "react" +import { Hits, InstantSearch, SearchBox } from "react-instantsearch" +import { searchClient } from "../../../../lib/config" +import Modal from "../../../common/components/modal" +import { Button } from "@medusajs/ui" +import Image from "next/image" +import Link from "next/link" +import { usePathname } from "next/navigation" + +type Hit = { + objectID: string; + id: string; + title: string; + description: string; + handle: string; + thumbnail: string; +} + +export default function SearchModal() { + const [isOpen, setIsOpen] = useState(false) + const pathname = usePathname() + + useEffect(() => { + setIsOpen(false) + }, [pathname]) + + return ( + <> +
+ +
+ setIsOpen(false)}> + + + + + + + ) +} + +const Hit = ({ hit }: { hit: Hit }) => { + return ( +
+ {hit.title} +
+

{hit.title}

+

{hit.description}

+
+ +
+ ) +} +``` + +You create a `SearchModal` component that displays a search box and the search results using widgets from Algolia's `react-instantsearch` library. + +To display each result item (or hit), you create a `Hit` component that displays the product's title, description, and thumbnail. You also add a link to the product's page. + +Finally, you show the search modal when the customer clicks a "Search" button, which you'll add to the navigation bar next. + +### Add Search Button to Navigation Bar + +The last step is to show the search button in the navigation bar. + +In `src/modules/layout/templates/nav/index.tsx`, add the following imports at the top of the file: + +```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue" +import SearchModal from "@modules/search/components/modal" +``` + +Then, in the return statement of the `Nav` component, add the `SearchModal` component before the `div` surrounding the "Account" link: + +```tsx title="src/modules/layout/templates/nav/index.tsx" badgeLabel="Storefront" badgeColor="blue" + +``` + +The search button will now appear in the navigation bar before the Account link. + +### Test it Out + +To test out the storefront changes and the search API route, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +Then, start the Next.js Starter Storefront from its directory: + +```bash npm2yarn +npm run dev +``` + +Next, go to `localhost:8000`. You'll find a Search button at the top right of the navigation bar. If you click on it, you can search through your products. You can also click on a product to view its page. + +![The Next.js Starter Storefront showing the search modal with search results](https://res.cloudinary.com/dza7lstvk/image/upload/v1742827777/Medusa%20Resources/Screenshot_2025-03-24_at_4.49.23_PM_kzhldx.png) *** ## Next Steps -You've now integrated Medusa with Resend. You can add more templates for other emails, such as customer registration confirmation, user invites, and more. Check out the [Events Reference](https://docs.medusajs.com/references/events/index.html.md) for a list of all events that the Medusa application emits. +You've now integrated Algolia with Medusa and added search functionality to your storefront. You can expand on these features to: + +- Add filters to the search results. You can do that using Algolia's [widgets](https://www.algolia.com/doc/guides/building-search-ui/widgets/showcase/react/) and customizing the search API route in Medusa to accept filter parameters. +- Support indexing other data types, such as product categories. You can create the subscribers and workflows for categories similar to products. 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. @@ -62716,354 +64787,354 @@ To learn more about the commerce features that Medusa provides, check out Medusa ## JS SDK Admin +- [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) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancel/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.cancelRequest/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/index.html.md) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/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) +- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/index.html.md) +- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.removeOutboundItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.retrieve/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) +- [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundShipping/index.html.md) +- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/index.html.md) +- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/index.html.md) +- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/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/Campaign/methods/js_sdk.admin.Campaign.retrieve/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.create/index.html.md) - [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) - [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) - [revoke](https://docs.medusajs.com/references/js_sdk/admin/ApiKey/methods/js_sdk.admin.ApiKey.revoke/index.html.md) -- [batchPromotions](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.batchPromotions/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/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) -- [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) -- [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/Campaign/methods/js_sdk.admin.Campaign.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Campaign/methods/js_sdk.admin.Campaign.update/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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Currency/methods/js_sdk.admin.Currency.retrieve/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) -- [fetchStream](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetchStream/index.html.md) -- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) - [clearToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.clearToken/index.html.md) -- [getTokenStorageInfo\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getTokenStorageInfo_/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) - [getJwtHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getJwtHeader_/index.html.md) -- [getPublishableKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getPublishableKeyHeader_/index.html.md) +- [getApiKeyHeader\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getApiKeyHeader_/index.html.md) +- [fetch](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.fetch/index.html.md) - [getToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.getToken_/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) - [initClient](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.initClient/index.html.md) -- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [setToken\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken_/index.html.md) +- [setToken](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.setToken/index.html.md) - [throwError\_](https://docs.medusajs.com/references/js_sdk/admin/Client/methods/js_sdk.admin.Client.throwError_/index.html.md) -- [addInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundShipping/index.html.md) -- [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addInboundItems/index.html.md) -- [addOutboundItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundItems/index.html.md) -- [addItems](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addItems/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.addOutboundShipping/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.create/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) -- [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) -- [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.deleteOutboundShipping/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) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateInboundItem/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) -- [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateOutboundItem/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) -- [updateItem](https://docs.medusajs.com/references/js_sdk/admin/Claim/methods/js_sdk.admin.Claim.updateItem/index.html.md) -- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/index.html.md) +- [getItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.getItem/index.html.md) +- [setItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.setItem/index.html.md) +- [removeItem](https://docs.medusajs.com/references/js_sdk/admin/CustomStorage/methods/js_sdk.admin.CustomStorage.removeItem/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.create/index.html.md) +- [batchCustomers](https://docs.medusajs.com/references/js_sdk/admin/CustomerGroup/methods/js_sdk.admin.CustomerGroup.batchCustomers/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) - [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) - [batchCustomerGroups](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.batchCustomerGroups/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.create/index.html.md) - [createAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.createAddress/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.list/index.html.md) -- [deleteAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.deleteAddress/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.delete/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) - [listAddresses](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.listAddresses/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) - [retrieveAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.retrieveAddress/index.html.md) -- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) -- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.update/index.html.md) - [addPromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addPromotions/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) -- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md) +- [addShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addShippingMethod/index.html.md) +- [updateAddress](https://docs.medusajs.com/references/js_sdk/admin/Customer/methods/js_sdk.admin.Customer.updateAddress/index.html.md) +- [addItems](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.addItems/index.html.md) - [confirmEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.confirmEdit/index.html.md) +- [cancelEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.cancelEdit/index.html.md) +- [beginEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.beginEdit/index.html.md) +- [convertToOrder](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.convertToOrder/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.list/index.html.md) -- [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md) - [removeActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionItem/index.html.md) -- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/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) +- [removeActionShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeActionShippingMethod/index.html.md) - [removeShippingMethod](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removeShippingMethod/index.html.md) -- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md) +- [removePromotions](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.removePromotions/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.retrieve/index.html.md) +- [requestEdit](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.requestEdit/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) -- [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) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/index.html.md) -- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) +- [updateActionItem](https://docs.medusajs.com/references/js_sdk/admin/DraftOrder/methods/js_sdk.admin.DraftOrder.updateActionItem/index.html.md) - [addInboundItems](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addInboundItems/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) - [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) +- [addOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.addOutboundShipping/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancel/index.html.md) - [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.cancelRequest/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.create/index.html.md) - [deleteOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteOutboundShipping/index.html.md) -- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.list/index.html.md) +- [deleteInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.deleteInboundShipping/index.html.md) - [removeInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeInboundItem/index.html.md) -- [removeOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.removeOutboundItem/index.html.md) -- [request](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.request/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.retrieve/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) - [updateOutboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundItem/index.html.md) -- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) - [updateInboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundShipping/index.html.md) +- [updateInboundItem](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateInboundItem/index.html.md) - [updateOutboundShipping](https://docs.medusajs.com/references/js_sdk/admin/Exchange/methods/js_sdk.admin.Exchange.updateOutboundShipping/index.html.md) -- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) -- [updateServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.updateServiceZone/index.html.md) -- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/index.html.md) -- [retrieveServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.retrieveServiceZone/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.list/index.html.md) -- [resend](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.resend/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.retrieve/index.html.md) +- [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) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.createShipment/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) -- [create](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.cancel/index.html.md) - [batchUpdateLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.batchUpdateLevels/index.html.md) -- [listLevels](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.listLevels/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.list/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/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) +- [update](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Fulfillment/methods/js_sdk.admin.Fulfillment.create/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.create/index.html.md) +- [accept](https://docs.medusajs.com/references/js_sdk/admin/Invite/methods/js_sdk.admin.Invite.accept/index.html.md) - [updateLevel](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.updateLevel/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/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) +- [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/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.delete/index.html.md) +- [createServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.createServiceZone/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/InventoryItem/methods/js_sdk.admin.InventoryItem.delete/index.html.md) +- [deleteServiceZone](https://docs.medusajs.com/references/js_sdk/admin/FulfillmentSet/methods/js_sdk.admin.FulfillmentSet.deleteServiceZone/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) - [list](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.list/index.html.md) -- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) -- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) -- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) -- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) -- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/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) -- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/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) -- [retrievePreview](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.retrievePreview/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/index.html.md) -- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) -- [markAsPaid](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.markAsPaid/index.html.md) -- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) -- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) -- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Notification/methods/js_sdk.admin.Notification.retrieve/index.html.md) - [addItems](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.addItems/index.html.md) +- [cancelRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.cancelRequest/index.html.md) - [confirm](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.confirm/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.initiateRequest/index.html.md) - [removeAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.removeAddedItem/index.html.md) - [request](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.request/index.html.md) -- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) - [updateOriginalItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateOriginalItem/index.html.md) +- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancel/index.html.md) +- [cancelFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelFulfillment/index.html.md) +- [cancelTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.cancelTransfer/index.html.md) +- [createFulfillment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createFulfillment/index.html.md) +- [createShipment](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createShipment/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.list/index.html.md) +- [updateAddedItem](https://docs.medusajs.com/references/js_sdk/admin/OrderEdit/methods/js_sdk.admin.OrderEdit.updateAddedItem/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) +- [listLineItems](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.listLineItems/index.html.md) +- [requestTransfer](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.requestTransfer/index.html.md) +- [createCreditLine](https://docs.medusajs.com/references/js_sdk/admin/Order/methods/js_sdk.admin.Order.createCreditLine/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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.list/index.html.md) +- [refund](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.refund/index.html.md) +- [capture](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.capture/index.html.md) +- [listPaymentProviders](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.listPaymentProviders/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Payment/methods/js_sdk.admin.Payment.retrieve/index.html.md) - [batchPrices](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.batchPrices/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) -- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.create/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.list/index.html.md) +- [linkProducts](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.linkProducts/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Plugin/methods/js_sdk.admin.Plugin.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/PriceList/methods/js_sdk.admin.PriceList.update/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) -- [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) -- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.updateProducts/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.update/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.delete/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.retrieve/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/PricePreference/methods/js_sdk.admin.PricePreference.create/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) - [delete](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/PricePreference/methods/js_sdk.admin.PricePreference.create/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) - [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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/PaymentCollection/methods/js_sdk.admin.PaymentCollection.delete/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) +- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md) +- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) +- [createVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createVariant/index.html.md) +- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) +- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) +- [deleteOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteOption/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.delete/index.html.md) +- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) +- [deleteVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.deleteVariant/index.html.md) +- [import](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.import/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.list/index.html.md) +- [retrieveOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveOption/index.html.md) +- [listVariants](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listVariants/index.html.md) +- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.create/index.html.md) +- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/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) +- [updateProducts](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.updateProducts/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.create/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.delete/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.retrieve/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductCollection/methods/js_sdk.admin.ProductCollection.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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ProductCategory/methods/js_sdk.admin.ProductCategory.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.create/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.list/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.update/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductTag/methods/js_sdk.admin.ProductTag.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/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/ProductTag/methods/js_sdk.admin.ProductTag.delete/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.list/index.html.md) -- [batch](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batch/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.retrieve/index.html.md) -- [batchVariantInventoryItems](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.batchVariantInventoryItems/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/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) -- [createImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createImport/index.html.md) -- [confirmImport](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.confirmImport/index.html.md) -- [createOption](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.createOption/index.html.md) -- [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) -- [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) -- [export](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.export/index.html.md) -- [listOptions](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.listOptions/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) -- [retrieveVariant](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieveVariant/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.retrieve/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) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Product/methods/js_sdk.admin.Product.update/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/ProductVariant/methods/js_sdk.admin.ProductVariant.list/index.html.md) - [addRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.addRules/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ProductType/methods/js_sdk.admin.ProductType.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.delete/index.html.md) -- [listRuleAttributes](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleAttributes/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) +- [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) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.retrieve/index.html.md) +- [listRuleValues](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.listRuleValues/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) -- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/RefundReason/methods/js_sdk.admin.RefundReason.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.delete/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/Region/methods/js_sdk.admin.Region.update/index.html.md) +- [removeRules](https://docs.medusajs.com/references/js_sdk/admin/Promotion/methods/js_sdk.admin.Promotion.removeRules/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/index.html.md) -- [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) -- [batchProducts](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.batchProducts/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.delete/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/index.html.md) -- [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/RefundReason/methods/js_sdk.admin.RefundReason.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) - [create](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.create/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) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.delete/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/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) -- [cancel](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.cancel/index.html.md) -- [addReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.addReturnItem/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Reservation/methods/js_sdk.admin.Reservation.retrieve/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) +- [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) +- [list](https://docs.medusajs.com/references/js_sdk/admin/ReturnReason/methods/js_sdk.admin.ReturnReason.list/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) - [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) - [deleteReturnShipping](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.deleteReturnShipping/index.html.md) - [initiateReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateReceive/index.html.md) -- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) - [initiateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.initiateRequest/index.html.md) +- [confirmReceive](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.confirmReceive/index.html.md) +- [dismissItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.dismissItems/index.html.md) - [receiveItems](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.receiveItems/index.html.md) -- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) -- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) - [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) -- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) +- [removeReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReceiveItem/index.html.md) +- [removeReturnItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.removeReturnItem/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.retrieve/index.html.md) -- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) - [updateRequest](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateRequest/index.html.md) - [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) +- [updateReceiveItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateReceiveItem/index.html.md) +- [updateDismissItem](https://docs.medusajs.com/references/js_sdk/admin/Return/methods/js_sdk.admin.Return.updateDismissItem/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.list/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) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.retrieve/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.create/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/SalesChannel/methods/js_sdk.admin.SalesChannel.update/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/ShippingProfile/methods/js_sdk.admin.ShippingProfile.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.delete/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.update/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.create/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/ShippingProfile/methods/js_sdk.admin.ShippingProfile.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/index.html.md) +- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/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/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/Store/methods/js_sdk.admin.Store.update/index.html.md) +- [update](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.update/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.create/index.html.md) - [createFulfillmentSet](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.createFulfillmentSet/index.html.md) - [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) -- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) +- [updateRules](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.updateRules/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.update/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md) - [updateFulfillmentProviders](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateFulfillmentProviders/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.create/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.list/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/ShippingOption/methods/js_sdk.admin.ShippingOption.delete/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) -- [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) -- [list](https://docs.medusajs.com/references/js_sdk/admin/Store/methods/js_sdk.admin.Store.list/index.html.md) - [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.delete/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxProvider/methods/js_sdk.admin.TaxProvider.list/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.list/index.html.md) +- [updateSalesChannels](https://docs.medusajs.com/references/js_sdk/admin/StockLocation/methods/js_sdk.admin.StockLocation.updateSalesChannels/index.html.md) - [retrieve](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.retrieve/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) +- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.delete/index.html.md) - [list](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.list/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.create/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRate/methods/js_sdk.admin.TaxRate.update/index.html.md) -- [update](https://docs.medusajs.com/references/js_sdk/admin/TaxRegion/methods/js_sdk.admin.TaxRegion.update/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/TaxRegion/methods/js_sdk.admin.TaxRegion.retrieve/index.html.md) -- [create](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.create/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) -- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) -- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) -- [presignedUrl](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.presignedUrl/index.html.md) -- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) - [delete](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.delete/index.html.md) - [me](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.me/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.list/index.html.md) +- [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/User/methods/js_sdk.admin.User.retrieve/index.html.md) - [update](https://docs.medusajs.com/references/js_sdk/admin/User/methods/js_sdk.admin.User.update/index.html.md) +- [delete](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.delete/index.html.md) +- [presignedUrl](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.presignedUrl/index.html.md) +- [retrieve](https://docs.medusajs.com/references/js_sdk/admin/Upload/methods/js_sdk.admin.Upload.retrieve/index.html.md) +- [list](https://docs.medusajs.com/references/js_sdk/admin/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.list/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/WorkflowExecution/methods/js_sdk.admin.WorkflowExecution.retrieve/index.html.md) ## JS SDK Auth - [callback](https://docs.medusajs.com/references/js-sdk/auth/callback/index.html.md) -- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) - [login](https://docs.medusajs.com/references/js-sdk/auth/login/index.html.md) - [refresh](https://docs.medusajs.com/references/js-sdk/auth/refresh/index.html.md) - [resetPassword](https://docs.medusajs.com/references/js-sdk/auth/resetPassword/index.html.md) -- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) - [register](https://docs.medusajs.com/references/js-sdk/auth/register/index.html.md) +- [logout](https://docs.medusajs.com/references/js-sdk/auth/logout/index.html.md) +- [updateProvider](https://docs.medusajs.com/references/js-sdk/auth/updateProvider/index.html.md) ## JS SDK Store - [cart](https://docs.medusajs.com/references/js-sdk/store/cart/index.html.md) - [category](https://docs.medusajs.com/references/js-sdk/store/category/index.html.md) -- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) -- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [order](https://docs.medusajs.com/references/js-sdk/store/order/index.html.md) +- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) +- [collection](https://docs.medusajs.com/references/js-sdk/store/collection/index.html.md) - [payment](https://docs.medusajs.com/references/js-sdk/store/payment/index.html.md) - [product](https://docs.medusajs.com/references/js-sdk/store/product/index.html.md) -- [fulfillment](https://docs.medusajs.com/references/js-sdk/store/fulfillment/index.html.md) - [region](https://docs.medusajs.com/references/js-sdk/store/region/index.html.md) +- [customer](https://docs.medusajs.com/references/js-sdk/store/customer/index.html.md) # Admin Components & Layouts @@ -63085,6 +65156,65 @@ 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. +# Container - Admin Components + +The Medusa Admin wraps each section of a page in a container. + +![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) + +To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: + +```tsx +import { + Container as UiContainer, + clx, +} from "@medusajs/ui" + +type ContainerProps = React.ComponentProps + +export const Container = (props: ContainerProps) => { + return ( + + ) +} +``` + +The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. + +*** + +## Example + +Use that `Container` component in any widget or UI route. + +For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: + +```tsx title="src/admin/widgets/product-widget.tsx" +import { defineWidgetConfig } from "@medusajs/admin-sdk" +import { Container } from "../components/container" +import { Header } from "../components/header" + +const ProductWidget = () => { + return ( + +
+ + ) +} + +export const config = defineWidgetConfig({ + zone: "product.details.before", +}) + +export default ProductWidget +``` + +This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. + + # Action Menu - Admin Components The Medusa Admin often provides additional actions in a dropdown shown when users click a three-dot icon. @@ -63310,50 +65440,479 @@ export default ProductWidget ``` -# Single Column Layout - Admin Components +# Data Table - Admin Components -The Medusa Admin has pages with a single column of content. +This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). -This doesn't include the sidebar, only the main content. +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. -![An example of an admin page with a single column](https://res.cloudinary.com/dza7lstvk/image/upload/v1728286605/Medusa%20Resources/single-column.png) +![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) -To create a layout that you can use in UI routes to support one column of content, create the component `src/admin/layouts/single-column.tsx` with the following content: +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. -```tsx title="src/admin/layouts/single-column.tsx" -export type SingleColumnLayoutProps = { - children: React.ReactNode -} +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. -export const SingleColumnLayout = ({ children }: SingleColumnLayoutProps) => { - return ( -
- {children} -
- ) +## 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 } ``` -The `SingleColumnLayout` accepts the content in the `children` props. +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. -## Example +You've also added two memoized variables: -Use the `SingleColumnLayout` component in your UI routes that have a single column. For example: +- `offset`: How many items to skip when fetching data based on the current page. +- `statusFilters`: The selected status filters, if any. -```tsx title="src/admin/routes/custom/page.tsx" highlights={[["9"]]} +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" -import { Container } from "../../components/container" + +// ... + +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 { Header } from "../../components/header" +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 +
+ + + +
+
+ + +
) @@ -63367,146 +65926,6 @@ export const config = defineRouteConfig({ 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. - - -# Container - Admin Components - -The Medusa Admin wraps each section of a page in a container. - -![Example of a container in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728287102/Medusa%20Resources/container_soenir.png) - -To create a component that uses the same container styling in your widgets or UI routes, create the file `src/admin/components/container.tsx` with the following content: - -```tsx -import { - Container as UiContainer, - clx, -} from "@medusajs/ui" - -type ContainerProps = React.ComponentProps - -export const Container = (props: ContainerProps) => { - return ( - - ) -} -``` - -The `Container` component re-uses the component from the [Medusa UI package](https://docs.medusajs.com/ui/components/container/index.html.md) and applies to it classes to match the Medusa Admin's design conventions. - -*** - -## Example - -Use that `Container` component in any widget or UI route. - -For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container } from "../components/container" -import { Header } from "../components/header" - -const ProductWidget = () => { - return ( - -
- - ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This widget also uses a [Header](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/admin-components/components/header/index.html.md) custom component. - - -# 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. - # Forms - Admin Components @@ -64081,598 +66500,70 @@ 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 +# Section Row - Admin Components -This component is available after [Medusa v2.4.0+](https://github.com/medusajs/medusa/releases/tag/v2.4.0). +The Medusa Admin often shows information in rows of label-values, such as when showing a product's details. -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 section row in the Medusa Admin](https://res.cloudinary.com/dza7lstvk/image/upload/v1728292781/Medusa%20Resources/section-row_kknbnw.png) -![Example of a table in the product listing page](https://res.cloudinary.com/dza7lstvk/image/upload/v1728295658/Medusa%20Resources/list_ddt9zc.png) +To create a component that shows information in the same structure, create the file `src/admin/components/section-row.tsx` with the following content: -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. +```tsx title="src/admin/components/section-row.tsx" +import { Text, clx } from "@medusajs/ui" -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 = { +export type SectionRowProps = { title: string - subtitle?: string - actions?: ( - { - type: "button", - props: React.ComponentProps - link?: LinkProps - } | - { - type: "action-menu" - props: ActionMenuProps - } | - { - type: "custom" - children: React.ReactNode - } - )[] + value?: React.ReactNode | string | null + actions?: React.ReactNode } -export const Header = ({ - title, - subtitle, - actions = [], -}: HeadingProps) => { +export const SectionRow = ({ title, value, actions }: SectionRowProps) => { + const isValueString = typeof value === "string" || !value + return ( -
-
- {title} - {subtitle && ( - - {subtitle} - - )} -
- {actions.length > 0 && ( -
- {actions.map((action, index) => ( - <> - {action.type === "button" && ( - - )} - {action.type === "action-menu" && ( - - )} - {action.type === "custom" && action.children} - - ))} -
+
+ + {title} + + + {isValueString ? ( + + {value ?? "-"} + + ) : ( +
{value}
+ )} + + {actions &&
{actions}
}
) } ``` -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. +The `SectionRow` component shows a title and a value in the same row. 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. +- 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 `Header` component in any widget or UI route. +Use the `SectionRow` component in any widget or UI route. For example, create the widget `src/admin/widgets/product-widget.tsx` with the following content: @@ -64680,26 +66571,13 @@ For example, create the widget `src/admin/widgets/product-widget.tsx` with the f import { defineWidgetConfig } from "@medusajs/admin-sdk" import { Container } from "../components/container" import { Header } from "../components/header" +import { SectionRow } from "../components/section-row" const ProductWidget = () => { return ( -
{ - alert("You clicked the button.") - }, - }, - }, - ]} - /> +
+ ) } @@ -64711,7 +66589,7 @@ export const config = defineWidgetConfig({ 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. +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. # JSON View - Admin Components @@ -64942,98 +66820,6 @@ 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. @@ -65324,6 +67110,291 @@ If `data` isn't `undefined`, you display the `Table` component passing it the fo 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. +# 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. + + +# 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. + + # 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. @@ -65683,6 +67754,63 @@ The method returns an array with two items: 2. The second is the total count of records. +# retrieve Method - Service Factory Reference + +This method retrieves one record of the data model by its ID. + +## Retrieve a Record + +```ts +const post = await postModuleService.retrievePost("123") +``` + +### Parameters + +Pass the ID of the record to retrieve. + +### Returns + +The method returns the record as an object. + +*** + +## Retrieve a Record's Relations + +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](https://docs.medusajs.com/docs/learn/fundamentals/module-links/query/index.html.md). + +```ts +const post = await postModuleService.retrievePost("123", { + relations: ["author"], +}) +``` + +### Parameters + +To retrieve the data model with relations, pass as a second parameter of the method an object with the property `relations`. `relations`'s value is an array of relation names. + +### Returns + +The method returns the record as an object. + +*** + +## Select Properties to Retrieve + +```ts +const post = await postModuleService.retrievePost("123", { + select: ["id", "name"], +}) +``` + +### Parameters + +By default, all of the record's properties are retrieved. To select specific ones, pass in the second object parameter a `select` property. Its value is an array of property names. + +### Returns + +The method returns the record as an object. + + # 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). @@ -65770,63 +67898,6 @@ restoredPosts = { ``` -# 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. @@ -66784,6 +68855,168 @@ 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 +``` + + +# clx + +Utility function for working with classNames. + +## Usage + +*** + +The `clx` function is a utility function for working with classNames. It is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge) and is intended to be used with [Tailwind CSS](https://tailwindcss.com/). + +```tsx +import { clx } from "@medusajs/ui" + +type BoxProps = { + className?: string + children: React.ReactNode + mt: "sm" | "md" | "lg" +} + +const Box = ({ className, children, mt }: BoxProps) => { + return ( +
+ {children} +
+ ) +} + +``` + +In the above example the utility is used to apply a base style, a margin top that is dependent on the `mt` prop and a custom className. +The Box component accepts a `className` prop that is merged with the other classNames, and the underlying usage of `tw-merge` ensures that all Tailwind CSS classes are merged without style conflicts. + + # Alert A component for displaying important messages. @@ -73203,165 +75436,3 @@ If you're using the `Tooltip` component in a project other than the Medusa Admin - delayDuration: (number) The duration from when the pointer enters the trigger until the tooltip gets opened. Default: 100 - skipDelayDuration: (number) How much time a user has to enter another trigger without incurring a delay again. Default: 300 - 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. - -## Usage - -*** - -The `clx` function is a utility function for working with classNames. It is built using [clsx](https://www.npmjs.com/package/clsx) and [tw-merge](https://www.npmjs.com/package/tw-merge) and is intended to be used with [Tailwind CSS](https://tailwindcss.com/). - -```tsx -import { clx } from "@medusajs/ui" - -type BoxProps = { - className?: string - children: React.ReactNode - mt: "sm" | "md" | "lg" -} - -const Box = ({ className, children, mt }: BoxProps) => { - return ( -
- {children} -
- ) -} - -``` - -In the above example the utility is used to apply a base style, a margin top that is dependent on the `mt` prop and a custom className. -The Box component accepts a `className` prop that is merged with the other classNames, and the underlying usage of `tw-merge` ensures that all Tailwind CSS classes are merged without style conflicts. diff --git a/www/apps/resources/app/how-to-tutorials/tutorials/phone-auth/page.mdx b/www/apps/resources/app/how-to-tutorials/tutorials/phone-auth/page.mdx new file mode 100644 index 0000000000..186be8c0d2 --- /dev/null +++ b/www/apps/resources/app/how-to-tutorials/tutorials/phone-auth/page.mdx @@ -0,0 +1,2300 @@ +--- +sidebar_label: "Phone Authentication" +tags: + - name: auth + label: "Implement Phone Authentication" + - name: customer + label: "Implement Phone Authentication" + - server + - tutorial +--- + +import { Github, PlaySolid } from "@medusajs/icons" +import { Prerequisites, WorkflowDiagram, CardList } from "docs-ui" + +export const metadata = { + title: `Implement Phone Authentication and Integrate Twilio SMS`, +} + +# {metadata.title} + +In this tutorial, you will learn how to implement phone number authentication in your Medusa application. + +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](../../../commerce-modules/page.mdx), which are available out-of-the-box. These features include authentication with custom providers and for custom user or actor types. + +In this tutorial, you'll learn how to implement a custom authentication provider that allows customers to log in with their phone number. You'll also integrate [Twilio](https://www.twilio.com/en-us/messaging/channels/sms) to send SMS messages to those customers with the one-time password (OTP) for authentication. + + + +Twilio is just one option to deliver the OTP to the customer. You can integrate a different SMS provider or use a different method to send OTPs. + + + +## Summary + +By following this tutorial, you will learn how to: + +- Install and set up Medusa. +- Implement a custom phone authentication provider. +- Integrate Twilio to send OTPs by SMS. +- Customize the Next.js Starter Storefront to allow customers to log in with their phone numbers. + +You can follow this tutorial whether you're new to Medusa or an advanced Medusa developer. + +![Diagram showcasing the phone authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747744941/Medusa%20Resources/phone-auth-overview_bt5yrp.jpg) + + + +While this tutorial focuses on supporting phone authentication for customers, you can use the authentication provider for any actor type, such as admin user or vendor. [At the end of this tutorial](#next-steps), you'll learn how to authenticate other actor types. + + + + + +--- + +## Step 1: Install a Medusa Application + + + +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](../../../nextjs-starter/page.mdx), choose Yes. + +Afterward, 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](!docs!/learn/fundamentals/api-routes). Learn more in [Medusa's Architecture documentation](!docs!/learn/introduction/architecture). + + + +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](../../../troubleshooting/create-medusa-app-errors/page.mdx) for help. + + + +--- + +## Step 2: Implement Phone Authentication Module Provider + +In Medusa, you integrate custom authentication providers by creating an [Authentication Module Provider](../../../commerce-modules/auth/auth-providers/page.mdx). Then, you can use that provider to authenticate users using custom logic. + +In this step, you'll create a Phone Authentication Module Provider that allows users to log in with their phone numbers and an OTP. Later, you'll integrate Twilio to send the OTPs to the users, and customize the storefront to allow customers to log in with their phone numbers. + + + +An Authentication Module Provider doesn't need to handle storing and managing specific user details, such as creating customers or admin users. Instead, it only focuses on the logic of authenticating a type of user using custom logic or integration. You can learn more in the [Auth Module](../../../commerce-modules/auth/page.mdx) documentation. + + + +### Prerequisite: Install jsonwebtoken + +In the Phone Authentication Module Provider, you'll use the `jsonwebtoken` package to sign and verify the OTPs. + +To install the package, run the following command in the Medusa application directory: + +```bash npm2yarn +npm install jsonwebtoken +npm install @types/jsonwebtoken --save-dev +``` + +### a. Create Module Directory + +Modules are created under the `src/modules` directory. So, start by creating the directory `src/modules/phone-auth`. + +### b. Create Auth Module Provider Service + +A module has a service that contains its logic. For Authentication Module Providers, the service implements the logic to authenticate users. + +To create the service of the Phone Authentication Module Provider, create the file `src/modules/phone-auth/service.ts` with the following content: + +export const phoneAuthServiceHighlights = [ + ["10", "logger", "Resolve Logger class to log debug messages."], + ["11", "event_bus", "Resolve Event Module's service to emit events."], + ["15", "jwtSecret", "JWT secret to sign and verify OTPs."], + ["19", "DISPLAY_NAME", "Human-readable name for the provider."], + ["20", "identifier", "Unique identifier for the provider."], +] + +```ts title="src/modules/phone-auth/service.ts" highlights={phoneAuthServiceHighlights} +import { + AbstractAuthModuleProvider, + AbstractEventBusModuleService, +} from "@medusajs/framework/utils" +import { + Logger, +} from "@medusajs/types" + +type InjectedDependencies = { + logger: Logger + event_bus: AbstractEventBusModuleService +} + +type Options = { + jwtSecret: string +} + +class PhoneAuthService extends AbstractAuthModuleProvider { + static DISPLAY_NAME = "Phone Auth" + static identifier = "phone-auth" + private options: Options + private logger: Logger + private event_bus: AbstractEventBusModuleService + + constructor(container: InjectedDependencies, options: Options) { + // @ts-ignore + super(...arguments) + + this.options = options + this.logger = container.logger + this.event_bus = container.event_bus + } +} + +export default PhoneAuthService +``` + +An Authentication Module Provider's service must extend the `AbstractAuthModuleProvider` class. You'll get a type error about implementing the abstract methods of that class, which you'll add in the next steps. + +An Authentication Module Provider must also have the following static properties: + +- `identifier`: A unique identifier for the provider. +- `DISPLAY_NAME`: A human-readable name for the provider. This name is used for display purposes. + +A module provider's constructor receives two parameters: + +- `container`: The [module's container](!docs!/learn/fundamentals/modules/container) that contains Framework resources available to the module. You access the following resources: + - `logger`: A [Logger](!docs!/learn/debugging-and-testing/logging) class to log debug messages. + - `event_bus`: The [Event Module](../../../infrastructure-modules/event/page.mdx)'s service to emit events. +- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option: + - `jwtSecret`: A secret used to sign and verify the OTPs. + + + +You'll learn how to set this option when you [add the module provider to Medusa's configurations](#h-add-module-provider-to-medusas-configurations). + + + +In the constructor, you set the class's properties to the injected dependencies and options. + +In the next sections, you'll implement the methods of the `AbstractAuthModuleProvider` class. + + + +Refer to the [Create Auth Module Provider](/references/auth/provider) guide for detailed information about the methods. + + + +### c. Implement validateOptions Method + +The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start. + +So, add the `validateOptions` method to the `PhoneAuthService` class: + +```ts title="src/modules/phone-auth/service.ts" +// other imports... +import { + MedusaError, +} from "@medusajs/framework/utils" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + static validateOptions(options: Record): void | never { + if (!options.jwtSecret) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "JWT secret is required" + ) + } + } +} +``` + +The `validateOptions` method receives the options passed to the module provider as a parameter. + +In the method, you throw an error if the `jwtSecret` option is not set. + +### d. Implement register Method + +When a customer (or another actor type) registers in your application, they must also have an [auth identity](../../../commerce-modules/auth/auth-identity-and-actor-types/page.mdx) that allows them to login. + +The `register` method of an auth provider uses custom logic to create the auth identity for the actor type (such as customer). In the method, you can perform custom validation and specify the custom authentication details to store for the user's auth identity. + +Medusa uses the `register` method to create an auth identity that will be associated with the customer when they register. You can learn more in the [Authentication Flows](../../../commerce-modules/auth/auth-flows/page.mdx) documentation. + +![Diagram showcasing the relation between a customer and auth identity](https://res.cloudinary.com/dza7lstvk/image/upload/v1747747179/Medusa%20Resources/customer-auth-identity_je1bvh.jpg) + +So, add the `register` method to the `PhoneAuthService` class: + +export const registerHighlights = [ + ["14", "phone", "Extract phone from request body."], + ["24", "retrieve", "Check if user with phone number already exists."], + ["33", "create", "Create a new auth identity for the user."], +] + +```ts title="src/modules/phone-auth/service.ts" highlights={registerHighlights} +// other imports... +import { + AuthenticationInput, + AuthIdentityProviderService, + AuthenticationResponse, +} from "@medusajs/types" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async register( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone } = data.body || {} + + if (!phone) { + return { + success: false, + error: "Phone number is required", + } + } + + try { + await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + + return { + success: false, + error: "User with phone number already exists", + } + } catch (error) { + const user = await authIdentityProviderService.create({ + entity_id: phone, + }) + + return { + success: true, + authIdentity: user, + } + } + } +} +``` + +#### Parameters + +The `register` method receives an object parameter with the following properties: + +- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body. +- `authIdentityProviderService`: A service injected by the [Auth Module](../../../commerce-modules/auth/page.mdx) that allows you to manage auth identities. + + + +The method receives other parameters, which you can find in the [Create Auth Module Provider](/references/auth/provider#register) guide. + + + +#### Method Logic + +In the method, you extract the `phone` property from the request body, and return an error if it's not provided. You also return an error if another user is using the same phone number. + +Otherwise, you create a new auth identity for the user. You set the phone number as the `entity_id` of the auth identity, which is a unique identifier. + +#### Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the registration was successful. +- `authIdentity`: The created auth identity of the user. This property is only set if the registration was successful. +- `error`: An error message if the registration failed. + +### e. Implement authenticate Method + +When a customer (or another actor type) logs in, the `authenticate` method of an auth provider is called. This method uses custom logic to authenticate the user. + +Authentication providers may implement one of the following flows: + +- Direct authentication, where the user is authenticated using this method only. For example, authenticating with an email and password. +- Authentication with callback verification, where the user is authenticated using this method and then a callback is used to verify additional information. + +For the Phone Authentication Module Provider, you'll implement the second flow. The user will first be authenticated using the `authenticate` method to make sure the user exists and generate an OTP. Then, they need to supply the OTP to verify their identity. + +![Diagram showcasing authentication with callback verification](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749683/Medusa%20Resources/authentication-flow_ey59hw.jpg) + +So, add the `authenticate` method to the `PhoneAuthService` class: + +export const authenticateHighlights = [ + ["13", "phone", "Extract from request body."], + ["23", "retrieve", "Check that the user exists."], + ["33", "generateOTP", "Generate a 6-digit OTP."], + ["35", "update", "Store OTP in the provider metadat."], + ["41", "emit", "Emit an event with the generated OTP."], + ["51", "location", "Indicate that more actions are required."], + ["55", "generateOTP", "A method to generate the OTP"], + ["60", "logger", "Log the generated OTP for debugging."], + ["63", "expiresIn", "Expire the OTP in 60 seconds."] +] + +```ts title="src/modules/phone-auth/service.ts" highlights={authenticateHighlights} +// other imports... +import { + AuthIdentityDTO, +} from "@medusajs/types" +import jwt from "jsonwebtoken" + +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async authenticate( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone } = data.body || {} + + if (!phone) { + return { + success: false, + error: "Phone number is required", + } + } + + try { + await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + } catch (error) { + return { + success: false, + error: "User with phone number does not exist", + } + } + + const { hashedOTP, otp } = await this.generateOTP() + + await authIdentityProviderService.update(phone, { + provider_metadata: { + otp: hashedOTP, + }, + }) + + await this.event_bus.emit({ + name: "phone-auth.otp.generated", + data: { + otp, + phone, + }, + }, {}) + + return { + success: true, + location: "otp", + } + } + + async generateOTP(): Promise<{ hashedOTP: string, otp: string }> { + // Generate a 6-digit OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString() + + // for debug + this.logger.info(`Generated OTP: ${otp}`) + + const hashedOTP = jwt.sign({ otp }, this.options.jwtSecret, { + expiresIn: "60s", + }) + + return { hashedOTP, otp } + } +} +``` + +You add two methods: the `authenticate` method, and a helper `generateOTP` method. + +#### authenticate Parameters + +The `authenticate` method receives an object parameter with the following properties: + +- `data`: An object containing properties like `body` that holds request-body parameters. Clients will pass relevant authentication data, such as the user's phone number, in the request body. +- `authIdentityProviderService`: A service injected by the [Auth Module](../../../commerce-modules/auth/page.mdx) that allows you to manage auth identities. + + + +The method receives other parameters, which you can find in the [Create Auth Module Provider](/references/auth/provider#authenticate) guide. + + + +#### authenticate Logic + +In the method, you return an error if the `phone` property is not provided in the request body, or if a user with that phone number doesn't exist. + +Next, you generate a 6-digit OTP using the `generateOTP` method. Notice that you currently log the OTP for debugging purposes. You can remove this line later once you integrate Twilio. + +The OTP is hashed and stored in the `provider_metadata` property of the user's auth identity. The `provider_metadata` property is a JSON object that stores additional information about the auth identity. + +Then, you emit an event with the generated OTP and the user's phone number. This allows you later to handle the event and send the OTP to the user using services like Twilio. + +#### authenticate Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the authentication was successful. +- `location`: A string indicating a URL to perform additional actions in. In this case, you set the location to `otp`, indicating that the user should verify with the OTP. +- `error`: An error message if the authentication failed. + +### f. Implement validateCallback Method + +When an authentication provider requires a callback to verify the user, the Medusa application calls the `validateCallback` method. + +You can use this method to verify the OTP that the user entered. If valid, you return the logged in user, and the Medusa application will return a JWT token that the user can use to authenticate in the application. + +![Diagram showcasing how callback verification fits in the authentication flow](https://res.cloudinary.com/dza7lstvk/image/upload/v1747749734/Medusa%20Resources/callback-flow_yney5a.jpg) + +So, add the `validateCallback` method to the `PhoneAuthService` class: + +export const validateCallbackHighlights = [ + ["7", "phone", "Extract phone from request query."], + ["7", "otp", "Extract OTP from request query."], + ["16", "retrieve", "Check that the user exists."], + ["37", "verify", "Verify that the OTP is correct and hasn't expired."], + ["52", "update", "Remove OTP from provider metadata."], +] + +```ts title="src/modules/phone-auth/service.ts" highlights={validateCallbackHighlights} +class PhoneAuthService extends AbstractAuthModuleProvider { + // ... + async validateCallback( + data: AuthenticationInput, + authIdentityProviderService: AuthIdentityProviderService + ): Promise { + const { phone, otp } = data.query || {} + + if (!phone || !otp) { + return { + success: false, + error: "Phone number and OTP are required", + } + } + + const user = await authIdentityProviderService.retrieve({ + entity_id: phone, + }) + + if (!user) { + return { + success: false, + error: "User with phone number does not exist", + } + } + + // verify that OTP is correct + const userProvider = user.provider_identities?.find((provider) => provider.provider === this.identifier) + if (!userProvider || !userProvider.provider_metadata?.otp) { + return { + success: false, + error: "User with phone number does not have a phone auth provider", + } + } + + try { + const decodedOTP = jwt.verify( + userProvider.provider_metadata.otp as string, + this.options.jwtSecret + ) as { otp: string } + + if (decodedOTP.otp !== otp) { + throw new Error("Invalid OTP") + } + } catch (error) { + return { + success: false, + error: error.message || "Invalid OTP", + } + } + + const updatedUser = await authIdentityProviderService.update(phone, { + provider_metadata: { + otp: null, + }, + }) + + return { + success: true, + authIdentity: updatedUser, + } + } +} +``` + +#### Parameters + +The `validateCallback` method receives an object parameter with the following properties: + +- `data`: An object containing properties like `query` that holds query parameters. Clients will pass relevant authentication data, such as the user's phone number and OTP, in the request query. +- `authIdentityProviderService`: A service injected by the [Auth Module](../../../commerce-modules/auth/page.mdx) that allows you to manage auth identities. + + + +The method receives other parameters, which you can find in the [Create Auth Module Provider](/references/auth/provider#validatecallback) guide. + + + +#### Method Logic + +In the method, you return an error if the phone and otp aren't provided in the request query, or if a user with that phone number doesn't exist. + +Next, you verify that the OTP provided by the user is correct. You retrieve the hashed OTP from the `provider_metadata` property of the user's auth identity. If the OTP is not valid, you return an error. + + + +Since you set the hash expiration to 60 seconds, the OTP will be valid for 60 seconds. After that, the user will need to request a new OTP. + + + +After that, you update the user's auth identity to remove the OTP from the `provider_metadata` property. + +#### Return Value + +Finally, you return an object with the following properties: + +- `success`: A boolean indicating whether the authentication was successful. +- `authIdentity`: The user's auth identity. This property is only set if the authentication was successful. +- `error`: An error message if the authentication failed. + +### g. Export Module Definition + +You've now finished implementing the necessary methods for the Phone Authentication Module Provider. + +The final piece to a module is its definition, which you export in an `index.ts` file at the module's root directory. This definition tells Medusa the name of the module, its service, and optionally its loaders. + +To create the module's definition, create the file `src/modules/phone-auth/index.ts` with the following content: + +```ts title="src/modules/phone-auth/index.ts" +import PhoneAuthService from "./service" +import { + ModuleProvider, + Modules, +} from "@medusajs/framework/utils" + +export default ModuleProvider(Modules.AUTH, { + services: [PhoneAuthService], +}) +``` + +You use `ModuleProvider` from the Modules SDK to create the module provider's definition. It accepts two parameters: + +1. The name of the module that this provider belongs to, which is `Modules.AUTH` in this case. +2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as authentication providers in Medusa. + +### h. Add Module Provider to Medusa's Configurations + +Once you finish building the module, add it to Medusa's configurations to start using it. + +In `medusa-config.ts`, add a `modules` property: + +```ts title="medusa-config.ts" +// other imports... +import { Modules, ContainerRegistrationKeys } from "@medusajs/framework/utils" + +module.exports = defineConfig({ + // ... + modules: [ + { + resolve: "@medusajs/medusa/auth", + dependencies: [ + Modules.CACHE, + ContainerRegistrationKeys.LOGGER, + Modules.EVENT_BUS, + ], + options: { + providers: [ + // default provider + { + resolve: "@medusajs/medusa/auth-emailpass", + id: "emailpass", + }, + { + resolve: "./src/modules/phone-auth", + id: "phone-auth", + options: { + jwtSecret: process.env.PHONE_AUTH_JWT_SECRET || "supersecret", + }, + }, + ], + }, + }, + ], +}) +``` + +To pass an Auth Module Provider to the Auth Module, you add the `modules` property to the Medusa configuration and pass the Auth Module in its value. + +The Auth Module accepts a `dependencies` option, allowing you to inject dependencies into the containers of the module and its providers. The Auth Module requires passing the [Cache Module](../../../infrastructure-modules/cache/page.mdx) and Logger, but you also inject the `event_bus` dependency to use the Event Module's service in the Phone Authentication Module Provider. + +The Auth Module also accepts a `providers` option, which is an array of Auth Module Providers to register. You register the `emailpass` provider, which is registered by default when you don't provide any other providers. + +To register the Phone Authentication Module Provider, you add an object to the `providers` array with the following properties: + +- `resolve`: The NPM package or path to the module provider. In this case, it's the path to the `src/modules/phone-auth` directory. +- `id`: The ID of the module provider. The auth provider is then registered with the ID `au_{id}`. +- `options`: The options to pass to the module provider. These are the options you defined in the `Options` interface of the module provider's service. + +### i. Enable Phone Authentication for Customers + +By default, customers and admin users can be authenticated using the `emailpass` provider. When you add a new provider, you need to specify which actor types can use it. + +In `medusa-config.ts`, add to `projectConfig.http` a new `authMethodsPerActor` property: + +export const authMethodsPerActorHighlights = [ + ["8", "customer", "Enable the emailpass and phone-auth providers for customers."], +] + +```ts title="medusa-config.ts" highlights={authMethodsPerActorHighlights} +module.exports = defineConfig({ + projectConfig: { + // ... + http: { + // ... + authMethodsPerActor: { + user: ["emailpass"], + customer: ["emailpass", "phone-auth"], + }, + }, + }, + // ... +}) +``` + +The `authMethodsPerActor` property is an object whose keys are actor types. The values are arrays of authentication method IDs that can be used for that actor type. + +In this case, you enable the `phone-auth` provider for customers. You can also enable it for other actor types, such as admin users or vendors. + +### Test Out Phone Authentication + +In this section, you'll test out the Phone Authentication Module Provider using Medusa's API routes. You can, instead, test it out later using the [Next.js Starter Storefront](#step-4-use-phone-authentication-in-the-nextjs-starter-storefront). + +First, start the Medusa application with the following command: + +```bash npm2yarn +npm run start +``` + +#### Prerequisite: Retrieve Publishable API Key + +Before you start testing the authentication provider using the API routes, you need to retrieve your application's publishable API key. This key is necessary to send requests to API routes starting with `/store`. + +To retrieve the publishable API key: + +1. Open the Medusa Admin dashboard at `http://localhost:9000/admin` and log in. +2. Go to Settings -> Publishable API Keys. +3. Click on the API key in the table. +4. In its details page, click on the API key to copy it. + +![Publishable API Key page with the API key clicked](https://res.cloudinary.com/dza7lstvk/image/upload/fl_lossy/f_auto/r_16/ar_16:9,c_pad/v1/User%20Guide/Screenshot_2025-02-25_at_6.14.15_PM_muwq9e.png) + +#### a. Retrieve Registration Token + +The first step is to retrieve a registration token for a new customer. This token will allow them to register in the application. + +To retrieve the registration token, send a `POST` request to `/auth/customer/phone-auth/register`: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth/register' \ +--header 'Content-Type: application/json' \ +--data '{ + "phone": "+19077890116" +}' +``` + +Make sure to replace the phone number with the one you want to use. + +This will return a `token` in the response: + +```json title="Example Response" +{ + "token": "123..." +} +``` + +#### b. Register Customer + +Next, you'll register the customer using the [Register Customer](!api!/store#customers_postcustomers) API route. You'll pass the registration token you received in the previous step in the header of this request. + +So, send a `POST` request to `/store/customers`: + +```bash +curl -X POST 'http://localhost:9000/store/customers' \ +--header 'x-publishable-api-key: {publishable_api_key}' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer {reg_token}' \ +--data-raw '{ + "email": "+19077890116@gmail.com", + "phone": "19077890116", + "first_name": "John", + "last_name": "Smith" +}' +``` + +Make sure to replace: + +- `{publishable_api_key}` with the publishable API key you retrieved from the Medusa Admin dashboard. +- `{reg_token}` with the registration token you received in the previous step. +- The customer details in the request body with the ones you want to use. Use the same phone number you used in the previous step. + - You pass the email because it's required by the [Register Customer](!api!/store#customers_postcustomers) API route. You set it to the phone number with a `gmail.com` domain. + +The request will return the created customer's details: + +```json title="Example Response" +{ + "customer": { + "id": "cus_01JVPESW5SM1MSVPNM2MSC0ZEC", + "email": "+19077890116@gmail.com", + "company_name": null, + "first_name": "John", + "last_name": "Smith", + "phone": "19077890116", + "metadata": null, + "has_account": true, + "deleted_at": null, + "created_at": "2025-05-20T09:01:13.273Z", + "updated_at": "2025-05-20T09:01:13.273Z", + "addresses": [] + } +} +``` + +The customer can now authenticate using the phone number and OTP. + +#### c. Authenticate Customer + +Next, you'll authenticate the customer using the [Authenticate Customer](!api!/store#auth_postactor_typeauth_provider) API route. This would send the customer an OTP to their phone number (which you'll implement in the next step). + +So, send a `POST` request to `/auth/customer/phone-auth`: + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth' \ +--header 'Content-Type: application/json' \ +--data '{ + "phone": "+19077890116" +}' +``` + +Make sure to replace the phone number with the one you used to register the customer. + +This will return a `location` in the response: + +```json title="Example Response" +{ + "location": "otp" +} +``` + +Indicating that the user should verify their OTP. + + + +You can also use this route to resend the OTP if the user didn't receive it or if a minute has passed since the last OTP was sent. + + + +#### d. Verify OTP + +If you check the logs of the Medusa application, you'll see that the OTP was generated and logged: + +```bash +info: Generated OTP: 576794 +``` + +As mentioned before, this is only for debugging purposes. In the next step, you'll implement the logic to send the OTP to the user using Twilio. + +So, to verify the OTP, you'll send a request to the [Verify Callback API route](!api!/store#auth_postactor_typeauth_providercallback): + +```bash +curl -X POST 'http://localhost:9000/auth/customer/phone-auth/callback?phone=%2B19077890116&otp=476588' +``` + +You pass the following query parameters: + +- `phone`: The phone number of the customer. Make sure to use the same phone number you used to register the customer, and to encode it. For example, the `+` sign should be encoded as `%2B`. +- `otp`: The OTP that the customer received. Make sure to use the same OTP shown in the logs. + +If the OTP is valid, you'll receive a JWT token in the response: + +```json title="Example Response" +{ + "token": "123..." +} +``` + +You can use this token to authenticate the customer in the application. For example, you can use the token to [retrieve the customer's details](!api!/store#customers_getcustomersme). + + + +If the OTP has expired, send a request to the [Authenticate Customer](#c-authenticate-customer) API route to generate a new OTP + + + +--- + +## Step 3: Integrate Twilio SMS + +Similar to the Auth Module, the [Notification Module](../../../infrastructure-modules/notification/page.mdx) allows registering custom providers to send notifications, such as SMS or email. + +In this step, you'll create a Twilio Notification Module Provider, then use it to send the OTP to the customer. + + + +### a. Install Twilio SDK + +Before you start implementing the Twilio Notification Module Provider, install the Twilio SDK to interact with the Twilio API. + +Run the following command in the Medusa application directory: + +```bash npm2yarn +npm install twilio +``` + +You'll use the Twilio SDK in the Notification Module Provider's service. + +### b. Create Module Directory + +Create the directory `src/modules/twilio-sms` to create the Twilio Notification Module Provider. + +### c. Create Notification Module Provider Service + +A Notification Module Provider has a service that contains the sending logic. The service must extend the `AbstractNotificationProviderService` class. + +So, create the file `src/modules/twilio-sms/service.ts` with the following content: + +export const twilioSmsServiceHighlights = [ + ["8", "accountSid", "Twilio account SID."], + ["9", "authToken", "Twilio auth token."], + ["10", "from", "Twilio phone number to send the SMS from."], + ["14", "identifier", "Unique identifier for the module."], + ["15", "client", "Twilio client to send the SMS."], +] + +```ts title="src/modules/twilio-sms/service.ts" highlights={twilioSmsServiceHighlights} +import { + AbstractNotificationProviderService, +} from "@medusajs/framework/utils" +import { Twilio } from "twilio" + +type InjectedDependencies = {} + +type TwilioSmsServiceOptions = { + accountSid: string + authToken: string + from: string +} + +class TwilioSmsService extends AbstractNotificationProviderService { + static readonly identifier = "twilio-sms" + private readonly client: Twilio + private readonly from: string + + constructor(container: InjectedDependencies, options: TwilioSmsServiceOptions) { + super() + + this.client = new Twilio(options.accountSid, options.authToken) + this.from = options.from + } +} +``` + +You'll get a type error about implementing the abstract methods of the `AbstractNotificationProviderService` class, which you'll add in the next steps. + +A Notification Module Provider must have an `identifier` static property, which is a unique identifier for the module. This identifier is used to register the module in the Medusa application. + +A module provider's constructor receives two parameters: + +- `container`: The [module's container](!docs!/learn/fundamentals/modules/container) that contains Framework resources available to the module. You don't need to access any resources for this provider. +- `options`: Options that are passed to the module provider when it's registered in Medusa's configurations. You define the following option: + - `accountSid`: The Twilio account SID. + - `authToken`: The Twilio auth token. + - `from`: The Twilio phone number to send the SMS from. + + + +You'll learn how to set these options when you [add the module provider to Medusa's configurations](#g-add-module-provider-to-medusas-configurations). + + + +In the constructor, you set the class's properties to the injected dependencies and options. + +In the next sections, you'll implement the methods of the `AbstractNotificationProviderService` class. + + + +Refer to the [Create Notification Module Provider](/references/notification-provider-module) guide for detailed information about the methods. + + + +### d. Implement validateOptions Method + +The `validateOptions` method is used to validate the options passed to the module provider. If the method throws an error, the Medusa application won't start. + +So, add the `validateOptions` method to the `TwilioSmsService` class: + +```ts title="src/modules/twilio-sms/service.ts" +class TwilioSmsService extends AbstractNotificationProviderService { + // ... + static validateOptions(options: Record): void | never { + if (!options.accountSid) { + throw new Error("Account SID is required") + } + if (!options.authToken) { + throw new Error("Auth token is required") + } + if (!options.from) { + throw new Error("From is required") + } + } +} +``` + +The `validateOptions` method receives the options passed to the module provider as a parameter. + +In the method, you throw an error if any of the options are not set. + +### e. Implement send Method + +The only required method for a Notification Module Provider is the `send` method. When the Medusa application needs to send a notification using the provider's channel (such as SMS), it calls this method of the registered provider. + +So, add the `send` method to the `TwilioSmsService` class: + +export const sendHighlights = [ + ["12", "to", "Extract the phone number to send the SMS to."], + ["12", "content", "Extract the content of the SMS."], + ["12", "template", "Extract the template to use for the SMS."], + ["12", "data", "Extract the data to use in the template."], + ["13", "contentText", "Set the content text to either the text or template content."], + ["13", "getTemplateContent", "Get the content for the template."], + ["17", "message", "Send the SMS using Twilio's client."], + ["28", "getTemplateContent", "A method to get the content for the template."], +] + +```ts title="src/modules/twilio-sms/service.ts" highlights={sendHighlights} +// other imports... +import { + ProviderSendNotificationDTO, + ProviderSendNotificationResultsDTO, +} from "@medusajs/types" + +class TwilioSmsService extends AbstractNotificationProviderService { + // ... + async send( + notification: ProviderSendNotificationDTO + ): Promise { + const { to, content, template, data } = notification + const contentText = content?.text || await this.getTemplateContent( + template, data + ) + + const message = await this.client.messages.create({ + body: contentText, + from: this.from, + to, + }) + + return { + id: message.sid, + } + } + + async getTemplateContent( + template: string, + data?: Record | null + ): Promise { + switch (template) { + case "otp-template": + if (!data?.otp) { + throw new Error("OTP is required for OTP template") + } + + return `Your OTP is ${data.otp}` + default: + throw new Error(`Template ${template} not found`) + } + } +} +``` + +You implement the `send` method and a helper `getTemplateContent` method. + +#### send Parameters + +The `send` method receives an object parameter with the following properties: + +- `to`: The phone number to send the SMS to. +- `content`: An object containing the content of the SMS. The `text` property is the text to send. +- `template`: The template to use for the SMS. This is used to retrieve the fallback content of the SMS if `content.text` is not provided. +- `data`: An object containing the data to use in the template. This is used to replace placeholders in the template with actual values. + + + +The method receives other parameters, which you can find in the [Create Notification Module Provider](/references/notification-provider-module#send) guide. + + + +#### send Method Logic + +In the method, you set the SMS content either to the `text` property of the `content` object or to the template content. You define a `getTemplateContent` method that retrieves the content for a template. + +Then, you use the `messages.create` method of the Twilio client to send the SMS. You pass the following parameters: + +- `body`: The content of the SMS. +- `from`: The Twilio phone number to send the SMS from. +- `to`: The phone number to send the SMS to. + +#### send Return Value + +Finally, you return an object that has an `id` property with the ID of the sent SMS. This ID is stored in the notification record in the database. + +### f. Export Module Definition + +You've now finished implementing the necessary methods for the Twilio Notification Module Provider. You only need to export its definition. + +To create the module's definition, create the file `src/modules/twilio-sms/index.ts` with the following content: + +```ts title="src/modules/twilio-sms/index.ts" +import { + ModuleProvider, + Modules, +} from "@medusajs/framework/utils" +import TwilioSMSNotificationService from "./service" + +export default ModuleProvider(Modules.NOTIFICATION, { + services: [TwilioSMSNotificationService], +}) +``` + +You use `ModuleProvider` from the Modules SDK to create the module provider's definition passing it two parameters: + +1. The name of the module that this provider belongs to, which is `Modules.NOTIFICATION` in this case. +2. An object with a required property `services` indicating the module provider's services. Each of these services will be registered as notification providers in Medusa. + +### g. Add Module Provider to Medusa's Configurations + +You'll now add the Twilio Notification Module Provider to Medusa's configurations to start using it. + +In `medusa-config.ts`, add the following to the `modules` property: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + // ... + modules: [ + // ... + { + resolve: "@medusajs/medusa/notification", + options: { + providers: [ + // default provider + { + resolve: "@medusajs/medusa/notification-local", + id: "local", + options: { + name: "Local Notification Provider", + channels: ["feed"], + }, + }, + { + resolve: "./src/modules/twilio-sms", + id: "twilio-sms", + options: { + channels: ["sms"], + accountSid: process.env.TWILIO_ACCOUNT_SID, + authToken: process.env.TWILIO_AUTH_TOKEN, + from: process.env.TWILIO_FROM, + }, + }, + ], + }, + }, + ], +}) +``` + +You pass the Notification Module in the `modules` property to register the Twilio Notification Module Provider. + +The Notification Module accepts a `providers` option, which is an array of Notification Module Providers to register. You register the `local` provider, which is registered by default when you don't provide any other providers. + +To register the Twilio Notification Module Provider, you add an object with the following properties: + +- `resolve`: The path to the module provider. +- `id`: The ID of the module provider. The notification provider is then registered with the ID `np_{identifier}_{id}`. +- `options`: The options to pass to the module provider. These include the options you defined in the `Options` interface of the module provider's service. + - `channels`: The channels that the notification provider supports. In this case, you set it to `sms`, which is the channel used to send SMS notifications. + - `accountSid`: The Twilio account SID. + - `authToken`: The Twilio auth token. + - `from`: The Twilio phone number to send the SMS from. + +### h. Add Environment Variables + +To set the value of the Twilio options, add the following environment variables to your `.env` file: + +```shell title=".env" +TWILIO_ACCOUNT_SID=AC... +TWILIO_AUTH_TOKEN=05... +TWILIO_FROM=+1... +``` + +Where: + +- `TWILIO_ACCOUNT_SID`: The Twilio account SID. +- `TWILIO_AUTH_TOKEN`: The Twilio auth token. +- `TWILIO_FROM`: The Twilio phone number to send the SMS from. Make sure to use the phone number you purchased from Twilio. + +You can retrieve these information from the Twilio Console homepage. + +![Twilio console homepage showing the account SID, phone number, and auth token](https://res.cloudinary.com/dza7lstvk/image/upload/v1747736641/Medusa%20Resources/CleanShot_2025-05-20_at_13.22.55_2x_sztmfo.png) + +### i. Handle OTP Generated Event + +Now that you have integrated Twilio into Medusa, you can use it to send the OTP to the customer. To do that, you need to handle the `phone-auth.otp.generated` event that you emitted in the `authenticate` method of the Phone Authentication Module Provider. + +You can listen to events in a [subscriber](!docs!/learn/fundamentals/events-and-subscribers). A subscriber is an asynchronous function that listens to events to perform actions when the event is emitted. + +In this step, you'll create a subscriber that listens to the `phone-auth.otp.generated` event and sends an SMS to the customer with the OTP. + + + +Refer to the [Events and Subscribers](!docs!/learn/fundamentals/events-and-subscribers) documentation to learn more. + + + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, to create a subscriber, create the file `src/subscribers/send-otp.ts` with the following content: + +export const sendOtpHighlights = [ + ["14", "notificationModuleService", "Resolve the Notification Module's service."], + ["18", "createNotifications", "Send the OTP to the user using Twilio."], + ["29", `"phone-auth.otp.generated"`, "The event the subscriber is listening to."], +] + +```ts title="src/subscribers/send-otp.ts" highlights={sendOtpHighlights} +import { + SubscriberArgs, + type SubscriberConfig, +} from "@medusajs/medusa" +import { Modules } from "@medusajs/framework/utils" + +export default async function sendOtpHandler({ + event: { data: { + phone, + otp, + } }, + container, +}: SubscriberArgs<{ phone: string, otp: string }>) { + const notificationModuleService = container.resolve( + Modules.NOTIFICATION + ) + + await notificationModuleService.createNotifications({ + to: phone, + channel: "sms", + template: "otp-template", + data: { + otp, + }, + }) +} + +export const config: SubscriberConfig = { + event: "phone-auth.otp.generated", +} +``` + +The subscriber file must export: + +- An asynchronous subscriber function that's executed whenever the associated event is triggered. +- A configuration object with an event property whose value is the event the subscriber is listening to, which is `phone-auth.otp.generated`. + +The subscriber function accepts an object with the following properties: + +- `event`: An object with the event's data payload. In the `authenticate` method, you emitted the event with the following data: + - `phone`: The phone number of the user. + - `otp`: The OTP that was generated. +- `container`: The [Medusa container](!docs!/learn/fundamentals/medusa-container), which you can use to resolve Framework and commerce resources. + +In the subscriber function, you resolve the Notification Module's service from the Medusa container. Then, you use its `createNotifications` method to send the OTP to the user. + +Under the hood, the Notification Module's service delegates the sending to the Notification Module Provider of the `sms` channel, which is the Twilio Notification Module Provider in this case. + +The `createNotifications` method accepts an object with the following properties: + +- `to`: The phone number to send the notification to. You use the phone number from the event's data payload. +- `channel`: The channel to use to send the notification, which is `sms`. +- `template`: The template to use for the notification content, which is `otp-template`. +- `data`: An object containing the data to use in the template. You pass the OTP to the template. + +### j. Test it Out + +To test out the Twilio Notification Module Provider, you can follow the steps in the [Test Out Phone Authentication](#test-out-phone-authentication) section. + +After you authenticate the customer, the OTP will be sent to the customer's phone number using Twilio. Then, you can use the OTP to verify the authentication and receive a JWT token. + +Alternatively, you can also test it out after customizing the Next.js Starter Storefront, which you'll do in the next step. + + + +Make sure to remove the OTP logging line in the `generateOTP` method of the Phone Authentication Module Provider's service now that you have integrated Twilio. + + + +--- + +## Step 4: Use Phone Authentication in the Next.js Starter Storefront + +In this step, you'll customize the [Next.js Starter Storefront](../../../nextjs-starter/page.mdx) to allow customers to authenticate using their phone number and OTP. + +By default, the Next.js Starter Storefront supports email and password authentication. You'll replace it with phone authentication, but you can also keep both authentication methods if you want to. + + + +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-phone-auth`, you can find the storefront by going back to the parent directory and changing to the `medusa-phone-auth-storefront` directory: + +```bash +cd ../medusa-phone-auth-storefront # change based on your project name +``` + + + +### a. Install Phone Input Package + +To easily show a phone input where the user can enter their phone number, install the `react-phone-number-input` package: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm install react-phone-number-input +``` + +You'll use it in the login and registration forms to show a phone input. + +### b. Add Authenticate Function + +Before adding the forms, you'll add the functions that send requests to the Medusa API to authenticate the customer. + +The first one you'll add is the `authenticateWithPhone` function, which sends a request to the `/auth/customer/phone-auth` API route to authenticate the customer using their phone number. + +In `src/lib/data/customer.ts`, add the following function: + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" +export const authenticateWithPhone = async (phone: string) => { + try { + const response = await sdk.auth.login("customer", "phone-auth", { + phone, + }) + + if ( + typeof response === "string" || + !response.location || + response.location !== "otp" + ) { + throw new Error("Failed to login") + } + + return true + } catch (error: any) { + return error.toString() + } +} +``` + +The function accepts the phone number as a parameter. + +In the function, you use the [JS SDK](../../../js-sdk/page.mdx), which is configured within the Next.js Starter Storefront, to send a request to the `/auth/customer/phone-auth` API route. You pass the phone number in the request body. + +If the request doesn't return a `location` property set to `otp`, you throw an error. Otherwise, you return `true` to indicate that the request was successful. + +### c. add Verify OTP Function + +Next, you'll add the `verifyOTP` function, which sends a request to the `/auth/customer/phone-auth/callback` API route to verify the OTP. + +In `src/lib/data/customer.ts`, add the following function: + +export const verifyOtpHighlights = [ + ["9", "callback", "Verify the OTP using the callback API route."], + ["14", "setAuthToken", "Set the authentication token."], + ["17", "revalidateTag", "Revalidate the customer cache tag to refresh customer-related UI."], + ["19", "transferCart", "Transfer the cart from guest to authenticated customer."], +] + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={verifyOtpHighlights} +export const verifyOtp = async ({ + otp, + phone, +}: { + otp: string + phone: string +}) => { + try { + const token = await sdk.auth.callback("customer", "phone-auth", { + phone, + otp, + }) + + await setAuthToken(token) + + const customerCacheTag = await getCacheTag("customers") + revalidateTag(customerCacheTag) + + await transferCart() + + return true + } catch (e: any) { + return e.toString() + } +} +``` + +The function accepts an object with the following properties: + +- `otp`: The OTP to verify. +- `phone`: The phone number of the customer. + +In the function, you use the JS SDK to send a request to the `/auth/customer/phone-auth/callback` API route. You pass the phone number and OTP in the request body. + +If the request is successful and you receive a token, you set the token in the cookies and refresh the customer cache. This ensures that all customer-related UI is updated after logging in, such as showing the customer's profile when accessing the `/account` page. + +Then, you call the `transferCart` function to transfer the cart from the guest user to the authenticated customer. + +Finally, you return `true` to indicate that the request was successful. + +### d. Add Registration Function + +The last function you'll add is the `registerWithPhone` function, which will register the customer using their phone number. + +In `src/lib/data/customer.ts`, add the following function: + +export const registerWithPhoneHighlights = [ + ["11", "regToken", "Retrieve the registration token."], + ["20", "setAuthToken", "Set the authentication token."], + ["33", "create", "Create the customer."], + ["39", "authenticateWithPhone", "Authenticate the customer using their phone number."], +] + +```ts title="src/lib/data/customer.ts" badgeLabel="Storefront" badgeColor="blue" highlights={registerWithPhoneHighlights} +export const registerWithPhone = async ({ + firstName, + lastName, + phone, +}: { + firstName: string + lastName: string + phone: string +}) => { + try { + const { token: regToken } = await sdk.client.fetch< + { token: string } + >(`/auth/customer/phone-auth/register`, { + method: "POST", + body: { + phone, + }, + }) + + await setAuthToken(regToken as string) + const headers = { + ...(await getAuthHeaders()), + } + + const email = `${phone}@gmail.com` + const customerData = { + email, + first_name: firstName, + last_name: lastName, + phone, + } + + await sdk.store.customer.create( + customerData, + {}, + headers + ) + + return await authenticateWithPhone(phone) + } catch (error: any) { + return error.toString() + } +} +``` + +The function accepts an object with the following properties: + +- `firstName`: The first name of the customer. +- `lastName`: The last name of the customer. +- `phone`: The phone number of the customer. + +In the function, you retrieve a registration token for the customer using the `/auth/customer/phone-auth/register` API route. You pass the phone number in the request body. + +Then, after setting the registration token in the cookies, you create a customer using the [Create Customer](!api!/store#customers_postcustomers) API route. You pass the following properties in the request body: + +- `email`: The email of the customer. You set it to the phone number with a `@gmail.com` domain. +- `first_name`: The first name of the customer. +- `last_name`: The last name of the customer. +- `phone`: The phone number of the customer. + +Finally, you call the `authenticateWithPhone` function to authenticate the customer using their phone number. At this step, the customer would receive an OTP to login. + +### e. Add OTP Form + +Next, you'll add an OTP form that allows the customer to enter the OTP they receive after login or registration. Later, you'll reuse this form in both the login and registration pages. + +Create the file `src/modules/account/components/otp/index.tsx` with the following content: + +export const otpHighlights = [ + ["13", "otp", "The OTP entered by the customer."], + ["14", "error", "The error message to show if the OTP verification fails."], + ["15", "isLoading", "A boolean indicating whether the OTP verification is in progress."], + ["16", "countdown", "The countdown timer for resending the OTP."], + ["17", "inputRefs", "A ref to store the input elements for the OTP digits."], + ["19", "handleSubmit", "Verify OTP on submission."], + ["33", "handleResend", "Resend OTP to the customer."], + ["38", "handlePaste", "Improve paste experience for OTP input."], +] + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={otpHighlights} +"use client" + +import { Input } from "@medusajs/ui" +import { useState, useRef, useEffect } from "react" +import { authenticateWithPhone, verifyOtp } from "../../../../lib/data/customer" +import ErrorMessage from "../../../checkout/components/error-message" + +type Props = { + phone: string +} + +export const Otp = ({ phone }: Props) => { + const [otp, setOtp] = useState("") + const [error, setError] = useState("") + const [isLoading, setIsLoading] = useState(false) + const [countdown, setCountdown] = useState(60) + const inputRefs = useRef<(HTMLInputElement | null)[]>([]) + + const handleSubmit = async () => { + setIsLoading(true) + const response = await verifyOtp({ + otp, + phone, + }) + setOtp("") + setIsLoading(false) + + if (typeof response === "string") { + setError(response) + } + } + + const handleResend = async () => { + authenticateWithPhone(phone) + setCountdown(60) + } + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault() + const pastedData = e.clipboardData.getData("text") + const numericValue = pastedData.replace(/\D/g, "").slice(0, 6) + + if (numericValue) { + setOtp(numericValue) + // Focus the next empty input after pasted content + const nextEmptyIndex = Math.min(numericValue.length, 5) + inputRefs.current[nextEmptyIndex]?.focus() + } + } + + // TODO add use effects +} +``` + +You create an `Otp` component that accepts the phone number as a prop. + +In the component, you define the following state variables: + +- `otp`: The OTP entered by the customer. +- `error`: The error message to show if the OTP verification fails. +- `isLoading`: A boolean indicating whether the OTP verification is in progress. +- `countdown`: The countdown timer for resending the OTP. +- `inputRefs`: A ref to store the input elements for the OTP digits. You'll show six input elements for the OTP digits. + +You also define the following functions: + +- `handleSubmit`: This function is called when the customer submits the OTP. It calls the `verifyOtp` function to verify the OTP entered by the customer. +- `handleResend`: This function is called when the customer clicks the "Resend OTP" button that you'll add later. It calls the `authenticateWithPhone` function to resend the OTP to the customer's phone number. +- `handlePaste`: This function is called when the customer pastes the OTP in the input field. It improves the experience of pasting the OTP without having to enter it manually. + +#### Handle Variable Changes + +Next, you'll add `useEffect` hooks to handle changes in the state variables. + +Replace the `TODO` with the following: + +export const useEffectHighlights = [ + ["1", "useEffect", "Focus the first input element when the component mounts."], + ["7", "useEffect", "Automatically submit the OTP when the customer enters six digits."], + ["15", "useEffect", "Add an interval to update the countdown timer every second."], +] + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={useEffectHighlights} +useEffect(() => { + if (inputRefs.current[0]) { + inputRefs.current[0].focus() + } +}, [inputRefs.current]) + +useEffect(() => { + if (otp.length !== 6 || isLoading) { + return + } + + handleSubmit() +}, [otp, isLoading]) + +useEffect(() => { + const timer = setInterval(() => { + setCountdown((prev) => { + return prev > 0 ? prev - 1 : 0 + }) + }, 1000) + + return () => clearInterval(timer) +}, []) + +// TODO render form +``` + +You add three `useEffect` hooks: + +1. The first one focuses the first input element when the component mounts. +2. The second one automatically submits the OTP when the customer enters six digits. +3. The third one adds an interval to update the countdown timer every second. + +### f. Render OTP Form + +Lastly, you'll render the OTP form with the input elements and the resend button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/otp/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

+ Verify Phone Number +

+

+ Enter the code sent to your phone number to login. +

+
+ {[...Array(6)].map((_, index) => ( + { + inputRefs.current[index] = el + }} + onPaste={handlePaste} + value={otp[index] || ""} + onChange={(e) => { + const elm = e.target + const value = elm.value + setOtp((prev) => { + const newOtp = prev.split("") + newOtp[index] = value + return newOtp.join("") + }) + if (value && /^\d+$/.test(value)) { + // Move focus to next input + const nextInput = elm.parentElement?.nextElementSibling?.querySelector("input") + nextInput?.focus() + } + }} + onKeyDown={(e) => { + if (e.key === "Backspace" && !e.currentTarget.value) { + // Move focus to previous input on backspace + const prevInput = e.currentTarget.parentElement?.previousElementSibling?.querySelector("input") + prevInput?.focus() + } + }} + /> + ))} +
+
+ +
+ +
+) +``` + +You show six input elements for the OTP digits. When a value is entered in an input, the focus moves to the next input. In addition, when the customer presses backspace on an empty input, the focus moves to the previous input. + +You also show a resend button that allows the customer to resend the OTP once the countdown timer reaches zero. + +You now have an OTP form that you can use in both the login and registration pages. + +### g. Add Registration Form + +You'll now add a registration form that allows the customer to register using their phone number. + +First, in `src/modules/account/templates/login-template.tsx`, update the `LOGIN_VIEW` to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={[["4"], ["5"]]} +export enum LOGIN_VIEW { + SIGN_IN = "sign-in", + REGISTER = "register", + REGISTER_PHONE = "register-phone", + SIGN_IN_PHONE = "sign-in-phone", +} +``` + +By default, the login template supports switching between the login and registration views for email and password authentication. With the above change, you add two new views: login and registration with phone number. + +Then, to add the registration form, create the file `src/modules/account/components/register-phone/index.tsx` with the following content: + +export const registerPhoneHighlights = [ + ["20", "firstName", "The first name of the customer."], + ["21", "lastName", "The last name of the customer."], + ["22", "phone", "The phone number of the customer."], + ["23", "error", "The error message to show if the registration fails."], + ["24", "loading", "A boolean indicating whether the registration is in progress."], + ["25", "enterOtp", "A boolean indicating whether to show the OTP form."], + ["26", "countryCode", "The country code of the customer."], + ["28", "handleSubmit", "Register customer on form submision."], + ["45", "enterOtp", "Show the OTP form after successful registration."], +] + +```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={registerPhoneHighlights} +"use client" + +import { useState } from "react" +import Input from "@modules/common/components/input" +import { LOGIN_VIEW } from "@modules/account/templates/login-template" +import ErrorMessage from "@modules/checkout/components/error-message" +import LocalizedClientLink from "@modules/common/components/localized-client-link" +import { registerWithPhone } from "@lib/data/customer" +import "react-phone-number-input/style.css" +import PhoneInput from "react-phone-number-input" +import { Otp } from "../otp" +import { useParams } from "next/navigation" +import { Button } from "@medusajs/ui" + +type Props = { + setCurrentView: (view: LOGIN_VIEW) => void +} + +const RegisterPhone = ({ setCurrentView }: Props) => { + const [firstName, setFirstName] = useState("") + const [lastName, setLastName] = useState("") + const [phone, setPhone] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + const [enterOtp, setEnterOtp] = useState(false) + const { countryCode } = useParams() as { countryCode: string } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + const response = await registerWithPhone({ + firstName, + lastName, + phone, + }) + setLoading(false) + if (typeof response === "string") { + setError(response) + return + } + + setEnterOtp(true) + } + + if (enterOtp) { + return + } + + // TODO render form +} + +export default RegisterPhone +``` + +You create a `RegisterPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views. + +In the component, you define the following state variables: + +- `firstName`, `lastName`, and `phone` to store the form inputs' values. +- `error`: The error message to show if the registration fails. +- `loading`: A boolean indicating whether the registration is in progress. +- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled once the customer is registered and they need to authenticate using the OTP. +- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country. + +You also define a `handleSubmit` function that handles the form submission. It calls the `registerWithPhone` function to register the customer using their phone number. + +If the registration is successful, you set `enterOtp` to `true` to show the OTP form. Otherwise, you set the error message. + +#### Render Registration Form + +Next, you'll render the registration form with the input fields and the submit button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/register-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

+ Become a Medusa Store Member +

+

+ Create your Medusa Store Member profile, and get access to an enhanced + shopping experience. +

+
+
+ setFirstName(e.target.value)} + /> + setLastName(e.target.value)} + /> + setPhone(value as string)} + name="phone" + required + autoComplete="off" + // @ts-ignore + defaultCountry={countryCode.toUpperCase()} + /> +
+ + + By creating an account, you agree to Medusa Store's{" "} + + Privacy Policy + {" "} + and{" "} + + Terms of Use + + . + + + + + Already a member?{" "} + + . + +
+) +``` + +You render the registration form with input fields for the first name, last name, and phone number. + +For the phone number input, you use the `PhoneInput` component from the `react-phone-number-input` package. You set the `defaultCountry` prop to the country code retrieved from the URL parameters. + +You also show a submit button that calls the `handleSubmit` function when clicked, and a button to switch to the login form. + +#### Add to Login Template + +Next, you'll add the `RegisterPhone` component to the login template. + +In `src/modules/account/templates/login-template.tsx`, add the following import: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +import RegisterPhone from "@modules/account/components/register-phone" +``` + +Then, change the `return` statement of the `LoginTemplate` component to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+ {currentView === "sign-in" ? ( + + ) : currentView === "register" ? ( + + ) : currentView === "register-phone" ? ( + + ) : ( + // TODO: Add login phone view + <> + )} +
+) +``` + +You show the registration form when the `currentView` is set to `register-phone`. You'll also add the login form later. + +### h. Add Login Form + +Next, you'll add a login form that allows the customer to log in using their phone number. + +To create the form, create the file `src/modules/account/components/login-phone/index.tsx` with the following content: + +export const loginPhoneHighlights = [ + ["18", "phone", "The phone number entered by the customer."], + ["19", "error", "The error message to show if the login fails."], + ["20", "loading", "A boolean indicating whether the login is in progress."], + ["21", "enterOtp", "A boolean indicating whether to show the OTP form."], + ["22", "countryCode", "The country code of the customer."], + ["24", "handleSubmit", "Authenticate customer on form submission."], + ["37", "enterOtp", "Show the OTP form after successful login."], +] + +```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" highlights={loginPhoneHighlights} +"use client" + +import { authenticateWithPhone } from "@lib/data/customer" +import { LOGIN_VIEW } from "@modules/account/templates/login-template" +import ErrorMessage from "@modules/checkout/components/error-message" +import { useState } from "react" +import "react-phone-number-input/style.css" +import PhoneInput from "react-phone-number-input" +import { Otp } from "../otp" +import { useParams } from "next/navigation" +import { Button } from "@medusajs/ui" + +type Props = { + setCurrentView: (view: LOGIN_VIEW) => void +} + +const LoginPhone = ({ setCurrentView }: Props) => { + const [phone, setPhone] = useState("") + const [error, setError] = useState("") + const [loading, setLoading] = useState(false) + const [enterOtp, setEnterOtp] = useState(false) + const { countryCode } = useParams() as { countryCode: string } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + const response = await authenticateWithPhone(phone) + setLoading(false) + if (typeof response === "string") { + setError(response) + return + } + + setEnterOtp(true) + } + + if (enterOtp) { + return + } + + // TODO render form +} + +export default LoginPhone +``` + +You create a `LoginPhone` component that accepts a `setCurrentView` prop to switch between the login and registration views. + +In the component, you define the following state variables: + +- `phone`: The phone number entered by the customer. +- `error`: The error message to show if the login fails. +- `loading`: A boolean indicating whether the login is in progress. +- `enterOtp`: A boolean indicating whether to show the OTP form. This is enabled after the form is submitted. +- `countryCode`: The country code of the customer, which is retrieved from the URL parameters. You'll use this to show the phone input with a default selected country. + +You also define a `handleSubmit` function that handles the form submission. It calls the `authenticateWithPhone` function to authenticate the customer using their phone number. Then, it enables `enterOtp` to show the OTP form. + +#### Render Login Form + +Next, you'll render the login form with the input field and the submit button. + +Replace the `TODO` with the following: + +```tsx title="src/modules/account/components/login-phone/index.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+

Welcome back

+

+ Sign in to access an enhanced shopping experience. +

+
+
+ setPhone(value as string)} + name="phone" + required + // @ts-ignore + defaultCountry={countryCode.toUpperCase()} + /> +
+ {error && } + + + + Not a member?{" "} + + . + +
+) +``` + +You render the login form with an input field for the phone number. You also show a submit button that calls the `handleSubmit` function when clicked. + +### i. Add to Login Template + +Next, you'll add the `LoginPhone` component to the login template. + +In `src/modules/account/templates/login-template.tsx`, add the following import: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +import LoginPhone from "../components/login-phone" +``` + +Next, in the `LoginTemplate` component, change the default value of the `currentView` state to `LOGIN_VIEW.SIGN_IN_PHONE`: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +const [currentView, setCurrentView] = useState(LOGIN_VIEW.SIGN_IN_PHONE) +``` + +This ensures the phone login form is shown by default. + +Finally, replace the `return` statement of the `LoginTemplate` component to the following: + +```tsx title="src/modules/account/templates/login-template.tsx" badgeLabel="Storefront" badgeColor="blue" +return ( +
+ {currentView === "sign-in" ? ( + + ) : currentView === "register" ? ( + + ) : currentView === "register-phone" ? ( + + ) : ( + + )} +
+) +``` + +You show the login form when the `currentView` is set to `sign-in-phone`. + +### Test it Out + +You can now test out the phone authentication feature in the Next.js Starter Storefront. + +First, start the Medusa application by running the following command in the Medusa project's directory: + +```bash npm2yarn badgeLabel="Medusa Application" badgeColor="green" +npm run dev +``` + +Then, start the Next.js Starter Storefront by running the following command in the storefront project's directory: + +```bash npm2yarn badgeLabel="Storefront" badgeColor="blue" +npm run dev +``` + +Open your browser, navigate to `http://localhost:8000`, and click on the "Account" link at the top right. This will show the login form with just the phone number input. + +![Login form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739945/Medusa%20Resources/CleanShot_2025-05-20_at_14.18.36_2x_ubuika.png) + +You can also switch to the registration form by clicking on the "Join" link below the login form. + +![Registration form with phone number input](https://res.cloudinary.com/dza7lstvk/image/upload/v1747739988/Medusa%20Resources/CleanShot_2025-05-20_at_14.19.35_2x_btlc2s.png) + +You can try to login with the account you created before, or register with a new one. Once successful, you'll see the OTP form to enter the OTP you received as SMS. + +![OTP form](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740102/Medusa%20Resources/CleanShot_2025-05-20_at_14.21.18_2x_yrkuyg.png) + +After you enter the six digits, you'll be logged in and you'll see your profile page. + +![Profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740166/Medusa%20Resources/CleanShot_2025-05-20_at_14.22.30_2x_kshb83.png) + +--- + +## Step 5: Disallow Phone Updates + +The phone authentication feature is now complete, but there are two improvements you can make: + +1. Show the phone number in the profile page: Currently, it shows the email address, which is a fake address you've set. +2. Disable phone and email updates: Currently, the customer can update their phone number, which is not allowed for phone authentication. + +### a. Show Phone Number in Profile Page + +To show the phone number in the profile page instead of the email, in `src/modules/account/components/overview/index.tsx`, find the following in the `return` statement: + +```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue" + + {customer?.email} + +``` + +And replace it with the following: + +```tsx title="src/modules/account/components/overview/index.tsx" badgeLabel="Storefront" badgeColor="blue" + + {customer?.phone} + +``` + +If you check the profile page now, you'll see the phone number instead of the email address at the top right. + +![Phone number showing in profile page](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740378/Medusa%20Resources/CleanShot_2025-05-20_at_14.25.59_2x_x0pxvt.png) + +### b. Remove Email and Phone Fields + +Next, you'll remove the fields to update email and phone number from the profile page. + +In `src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx`, find the following lines to remove from the `return` statement: + +```tsx title="src/app/[countryCode]/(main)/account/@dashboard/profile/page.tsx" badgeLabel="Storefront" badgeColor="blue" +
+ {/* ... */} + {/* Remove the following */} + + + + + {/* ... */} +
+``` + +If you go to your profile page and click on "Profile" in the sidebar, the email and phone number sections will be removed. + +![Profile page without email and phone fields](https://res.cloudinary.com/dza7lstvk/image/upload/v1747740515/Medusa%20Resources/CleanShot_2025-05-20_at_14.28.09_2x_ugab7d.png) + +### c. Disable Phone Updates in Medusa + +While removing the email and phone fields from the profile page prevents customers using the storefront from updating their phone number, it doesn't prevent them from updating it using Medusa's API. + +In this section, you'll add a [middleware](!docs!/learn/fundamentals/api-routes/middlewares) to the `/store/customers/me` API route that prevents customers from updating their phone number. + +A middleware is a function that's executed whenever a request is sent to an API route. It's executed before the route handler, allowing you to validate requests, apply authentication guards, and more. + + + +Learn more in the [Middlewares](!docs!/learn/fundamentals/api-routes/middlewares) documentation. + + + +To add a middleware in your Medusa application, create the file `src/api/middlewares.ts` with the following content: + +```ts title="src/api/middlewares.ts" badgeLabel="Medusa Application" badgeColor="green" +import { defineMiddlewares } from "@medusajs/framework/http" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/store/customers/me", + method: ["POST"], + middlewares: [ + async (req, res, next) => { + const { phone } = req.body as Record + + if (phone) { + return res.status(400).json({ + error: "Phone number is not allowed to be updated", + }) + } + + next() + }, + ], + }, + ], +}) +``` + +You define middlewares using the `defineMiddlewares` function. It accepts an object having a `routes` property that holds all middlewares applied to API routes. + +Each object in `routes` has the following properties: + +- `matcher`: The API route path to apply the middleware on. You set it to `/store/customers/me`. +- `method`: The HTTP method to apply the middleware to. You set it to `POST` so that the middleware is applied only on `POST` requests sent to the `/store/customers/me` route. +- `middlewares`: An array of middlewares to apply on the route. You add a middleware that throws an error if the request body contains a `phone` property. This prevents customers from updating their phone number using the API. + +Any `POST` request to the `/store/customers/me` route will now be validated to ensure it's not updating the phone number. + +--- + +## Next Steps + +You've now implemented phone authentication in Medusa with Twilio integration. You can further customize the phone authentication feature based on your business use case. + +### Authenticate Other Actor Types + +This tutorial focused on authenticating customers using their phone number. However, you can also authenticate other actor types, such as admin users and vendors. + +To do that, first, enable the `phone-auth` authentication strategy in `medusa-config.ts` for the actor types. For example: + +```ts title="medusa-config.ts" +module.exports = defineConfig({ + projectConfig: { + // ... + http: { + // ... + authMethodsPerActor: { + user: ["emailpass", "phone-auth"], + customer: ["emailpass", "phone-auth"], + vendor: ["emailpass", "phone-auth"], + }, + }, + }, +}) +``` + +Then, when sending requests to the authentication API routes mentioned in the [Test Out Phone Authentication](#test-out-phone-authentication) section, replace `customer` in the API route paths with the actor type you want to authenticate: + +- `/auth/customer/phone-auth/register` -> `/auth/user/phone-auth/register` +- `/auth/customer/phone-auth` -> `/auth/user/phone-auth` +- `/auth/customer/phone-auth/callback` -> `/auth/user/phone-auth/callback` + +Finally, update the UI to show the phone authentication option for the actor type you want to authenticate. This depends on the UI you're using, but you can follow an approach similar to the Next.js Starter Storefront customizations. + + + +The login form of Medusa Admin can't be customized, so you'll have to build a custom admin dashboard to support phone authentication for admin users. + + + +### Learn More about Medusa + +If you're new to Medusa, check out the [main documentation](!docs!/learn), 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](../../../commerce-modules/page.mdx). + +### Troubleshooting + +If you encounter issues during your development, check out the [troubleshooting guides](../../../troubleshooting/page.mdx). + +### Getting Help + +If you encounter issues not covered in the troubleshooting guides: + +1. Visit the [Medusa GitHub repository](https://github.com/medusajs/medusa) to report issues or ask questions. +2. Join the [Medusa Discord community](https://discord.gg/medusajs) for real-time support from community members. +3. Contact the [sales team](https://medusajs.com/contact/) to get help from the Medusa team. diff --git a/www/apps/resources/app/integrations/page.mdx b/www/apps/resources/app/integrations/page.mdx index d069d16e44..e15d62b232 100644 --- a/www/apps/resources/app/integrations/page.mdx +++ b/www/apps/resources/app/integrations/page.mdx @@ -167,6 +167,14 @@ A Notification Module Provider sends messages to users and customers in your Med variant: "blue", children: "Tutorial" } + }, + { + href: "/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms", + title: "Twilio SMS", + badge: { + variant: "blue", + children: "Tutorial" + } } ]} className="mb-1" diff --git a/www/apps/resources/generated/files-map.mjs b/www/apps/resources/generated/files-map.mjs index 93511e8b8f..cf6ceee6d5 100644 --- a/www/apps/resources/generated/files-map.mjs +++ b/www/apps/resources/generated/files-map.mjs @@ -743,6 +743,10 @@ export const filesMap = [ "filePath": "/www/apps/resources/app/how-to-tutorials/tutorials/loyalty-points/page.mdx", "pathname": "/how-to-tutorials/tutorials/loyalty-points" }, + { + "filePath": "/www/apps/resources/app/how-to-tutorials/tutorials/phone-auth/page.mdx", + "pathname": "/how-to-tutorials/tutorials/phone-auth" + }, { "filePath": "/www/apps/resources/app/how-to-tutorials/tutorials/product-reviews/page.mdx", "pathname": "/how-to-tutorials/tutorials/product-reviews" diff --git a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs index 149a972e33..343e8c0073 100644 --- a/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs +++ b/www/apps/resources/generated/generated-commerce-modules-sidebar.mjs @@ -471,7 +471,7 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "isPathHref": true, "type": "category", "title": "Server Guides", - "autogenerate_tags": "server+auth", + "autogenerate_tags": "auth+server", "autogenerate_as_ref": true, "sort_sidebar": "alphabetize", "description": "Learn how to use the Auth Module in your customizations on the Medusa application server.", @@ -499,6 +499,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "path": "/commerce-modules/auth/reset-password", "title": "Handle Password Reset Event", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Implement Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth", + "children": [] } ] }, @@ -2436,6 +2444,14 @@ const generatedgeneratedCommerceModulesSidebarSidebar = { "title": "Implement Loyalty Points", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "title": "Implement Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth", + "children": [] } ] }, diff --git a/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs b/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs index bb2fc7ca06..d1644d8bc5 100644 --- a/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs +++ b/www/apps/resources/generated/generated-how-to-tutorials-sidebar.mjs @@ -454,6 +454,15 @@ const generatedgeneratedHowToTutorialsSidebarSidebar = { "description": "Learn how to migrate data from Magento to Medusa.", "children": [] }, + { + "loaded": true, + "isPathHref": true, + "type": "link", + "title": "Phone Authentication", + "path": "/how-to-tutorials/tutorials/phone-auth", + "description": "Learn how to allow users to authenticate using their phone numbers.", + "children": [] + }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/generated/generated-integrations-sidebar.mjs b/www/apps/resources/generated/generated-integrations-sidebar.mjs index 8e934178ee..398dc7bb1c 100644 --- a/www/apps/resources/generated/generated-integrations-sidebar.mjs +++ b/www/apps/resources/generated/generated-integrations-sidebar.mjs @@ -153,6 +153,14 @@ const generatedgeneratedIntegrationsSidebarSidebar = { "path": "/infrastructure-modules/notification/sendgrid", "title": "SendGrid", "children": [] + }, + { + "loaded": true, + "isPathHref": true, + "type": "ref", + "path": "/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms", + "title": "Twilio SMS", + "children": [] } ] }, diff --git a/www/apps/resources/sidebars/auth.mjs b/www/apps/resources/sidebars/auth.mjs index efdc1168a3..f43f51cfc4 100644 --- a/www/apps/resources/sidebars/auth.mjs +++ b/www/apps/resources/sidebars/auth.mjs @@ -47,7 +47,7 @@ export const authSidebar = [ { type: "category", title: "Server Guides", - autogenerate_tags: "server+auth", + autogenerate_tags: "auth+server", autogenerate_as_ref: true, sort_sidebar: "alphabetize", description: diff --git a/www/apps/resources/sidebars/how-to-tutorials.mjs b/www/apps/resources/sidebars/how-to-tutorials.mjs index 0f655a166d..2c69633f1c 100644 --- a/www/apps/resources/sidebars/how-to-tutorials.mjs +++ b/www/apps/resources/sidebars/how-to-tutorials.mjs @@ -113,6 +113,13 @@ While tutorials show you a specific use case, they also help you understand how path: "/integrations/guides/magento", description: "Learn how to migrate data from Magento to Medusa.", }, + { + type: "link", + title: "Phone Authentication", + path: "/how-to-tutorials/tutorials/phone-auth", + description: + "Learn how to allow users to authenticate using their phone numbers.", + }, { type: "link", title: "Product Reviews", diff --git a/www/apps/resources/sidebars/integrations.mjs b/www/apps/resources/sidebars/integrations.mjs index 0ed3b6ae65..d0afec7747 100644 --- a/www/apps/resources/sidebars/integrations.mjs +++ b/www/apps/resources/sidebars/integrations.mjs @@ -105,6 +105,11 @@ export const integrationsSidebar = [ path: "/infrastructure-modules/notification/sendgrid", title: "SendGrid", }, + { + type: "ref", + path: "/how-to-tutorials/tutorials/phone-auth#step-3-integrate-twilio-sms", + title: "Twilio SMS", + }, ], }, { diff --git a/www/packages/tags/src/tags/auth.ts b/www/packages/tags/src/tags/auth.ts index bf18ea8f2f..e5599e5e32 100644 --- a/www/packages/tags/src/tags/auth.ts +++ b/www/packages/tags/src/tags/auth.ts @@ -7,6 +7,10 @@ export const auth = [ "title": "Reset Password", "path": "https://docs.medusajs.com/user-guide/reset-password" }, + { + "title": "Implement Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth" + }, { "title": "Log-out Customer in Storefront", "path": "https://docs.medusajs.com/resources/storefront-development/customers/log-out" diff --git a/www/packages/tags/src/tags/customer.ts b/www/packages/tags/src/tags/customer.ts index 3e915c772a..8ba01bbf9a 100644 --- a/www/packages/tags/src/tags/customer.ts +++ b/www/packages/tags/src/tags/customer.ts @@ -19,6 +19,10 @@ export const customer = [ "title": "Implement Loyalty Points", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points" }, + { + "title": "Implement Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth" + }, { "title": "Manage Customer Addresses in Storefront", "path": "https://docs.medusajs.com/resources/storefront-development/customers/addresses" diff --git a/www/packages/tags/src/tags/server.ts b/www/packages/tags/src/tags/server.ts index 1d76d377a7..5ce9ae7aaa 100644 --- a/www/packages/tags/src/tags/server.ts +++ b/www/packages/tags/src/tags/server.ts @@ -51,6 +51,10 @@ export const server = [ "title": "Loyalty Points", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points" }, + { + "title": "Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth" + }, { "title": "Product Reviews", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews" diff --git a/www/packages/tags/src/tags/tutorial.ts b/www/packages/tags/src/tags/tutorial.ts index eafc800819..79d998bd5c 100644 --- a/www/packages/tags/src/tags/tutorial.ts +++ b/www/packages/tags/src/tags/tutorial.ts @@ -31,6 +31,10 @@ export const tutorial = [ "title": "Loyalty Points", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/loyalty-points" }, + { + "title": "Phone Authentication", + "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/phone-auth" + }, { "title": "Product Reviews", "path": "https://docs.medusajs.com/resources/how-to-tutorials/tutorials/product-reviews"